diff --git a/.depcheckrc.yml b/.depcheckrc.yml
index 15f3c91b9..f7c189f91 100644
--- a/.depcheckrc.yml
+++ b/.depcheckrc.yml
@@ -18,6 +18,7 @@ ignores:
- '@metamask/auto-changelog' # invoked as `auto-changelog`
- '@metamask/forwarder'
- '@metamask/test-dapp'
+ - '@metamask/design-tokens' # Only imported in index.css
- '@sentry/cli' # invoked as `sentry-cli`
- 'chromedriver'
- 'depcheck' # ooo meta
@@ -34,6 +35,7 @@ ignores:
- '@storybook/core'
- '@storybook/addon-essentials'
- '@storybook/addon-a11y'
+ - 'storybook-dark-mode'
- 'style-loader'
- 'css-loader'
- 'sass-loader'
diff --git a/.eslintrc.babel.js b/.eslintrc.babel.js
new file mode 100644
index 000000000..b067db84b
--- /dev/null
+++ b/.eslintrc.babel.js
@@ -0,0 +1,9 @@
+module.exports = {
+ parser: '@babel/eslint-parser',
+ plugins: ['@babel'],
+ rules: {
+ '@babel/no-invalid-this': 'error',
+ // Prettier handles this
+ '@babel/semi': 'off',
+ },
+};
diff --git a/.eslintrc.base.js b/.eslintrc.base.js
new file mode 100644
index 000000000..47c379969
--- /dev/null
+++ b/.eslintrc.base.js
@@ -0,0 +1,67 @@
+const path = require('path');
+
+module.exports = {
+ extends: [
+ '@metamask/eslint-config',
+ path.resolve(__dirname, '.eslintrc.jsdoc.js'),
+ ],
+
+ globals: {
+ document: 'readonly',
+ window: 'readonly',
+ },
+
+ rules: {
+ 'default-param-last': 'off',
+ 'prefer-object-spread': 'error',
+ 'require-atomic-updates': 'off',
+
+ // This is the same as our default config, but for the noted exceptions
+ 'spaced-comment': [
+ 'error',
+ 'always',
+ {
+ markers: [
+ 'global',
+ 'globals',
+ 'eslint',
+ 'eslint-disable',
+ '*package',
+ '!',
+ ',',
+ // Local additions
+ '/:', // This is for our code fences
+ ],
+ exceptions: ['=', '-'],
+ },
+ ],
+
+ 'no-invalid-this': 'off',
+
+ // TODO: remove this override
+ 'padding-line-between-statements': [
+ 'error',
+ {
+ blankLine: 'always',
+ prev: 'directive',
+ next: '*',
+ },
+ {
+ blankLine: 'any',
+ prev: 'directive',
+ next: 'directive',
+ },
+ // Disabled temporarily to reduce conflicts while PR queue is large
+ // {
+ // blankLine: 'always',
+ // prev: ['multiline-block-like', 'multiline-expression'],
+ // next: ['multiline-block-like', 'multiline-expression'],
+ // },
+ ],
+
+ // It is common to import modules without assigning them to variables in
+ // a browser context. For instance, we may import polyfills which change
+ // global variables, or we may import stylesheets.
+ 'import/no-unassigned-import': 'off',
+ },
+};
diff --git a/.eslintrc.js b/.eslintrc.js
index 9c0bc07e9..84b68eada 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,132 +1,112 @@
+const path = require('path');
const { version: reactVersion } = require('react/package.json');
module.exports = {
root: true,
- parser: '@babel/eslint-parser',
- parserOptions: {
- sourceType: 'module',
- ecmaVersion: 2017,
- ecmaFeatures: {
- experimentalObjectRestSpread: true,
- impliedStrict: true,
- modules: true,
- blockBindings: true,
- arrowFunctions: true,
- objectLiteralShorthandMethods: true,
- objectLiteralShorthandProperties: true,
- templateStrings: true,
- classes: true,
- jsx: true,
- },
- },
-
+ // Ignore files which are also in .prettierignore
ignorePatterns: [
- '!.eslintrc.js',
- '!.mocharc.js',
- 'node_modules/**',
- 'dist/**',
- 'builds/**',
- 'test-*/**',
- 'docs/**',
- 'coverage/',
- 'jest-coverage/',
- 'development/chromereload.js',
'app/vendor/**',
- 'test/e2e/send-eth-with-private-key-test/**',
- 'nyc_output/**',
- '.vscode/**',
- 'lavamoat/*/policy.json',
- 'storybook-build/**',
+ 'builds/**/*',
+ 'dist/**/*',
+ 'development/chromereload.js',
],
-
- extends: ['@metamask/eslint-config', '@metamask/eslint-config-nodejs'],
-
- plugins: ['@babel', 'import', 'jsdoc'],
-
- globals: {
- document: 'readonly',
- window: 'readonly',
- },
-
- rules: {
- 'default-param-last': 'off',
- 'prefer-object-spread': 'error',
- 'require-atomic-updates': 'off',
-
- // This is the same as our default config, but for the noted exceptions
- 'spaced-comment': [
- 'error',
- 'always',
- {
- markers: [
- 'global',
- 'globals',
- 'eslint',
- 'eslint-disable',
- '*package',
- '!',
- ',',
- // Local additions
- '/:', // This is for our code fences
- ],
- exceptions: ['=', '-'],
- },
- ],
-
- 'import/no-unassigned-import': 'off',
-
- 'no-invalid-this': 'off',
- '@babel/no-invalid-this': 'error',
-
- // Prettier handles this
- '@babel/semi': 'off',
-
- 'node/no-process-env': 'off',
-
- // Allow tag `jest-environment` to work around Jest bug
- // See: https://github.com/facebook/jest/issues/7780
- 'jsdoc/check-tag-names': ['error', { definedTags: ['jest-environment'] }],
-
- // TODO: remove this override
- 'padding-line-between-statements': [
- 'error',
- {
- blankLine: 'always',
- prev: 'directive',
- next: '*',
- },
- {
- blankLine: 'any',
- prev: 'directive',
- next: 'directive',
- },
- // Disabled temporarily to reduce conflicts while PR queue is large
- // {
- // blankLine: 'always',
- // prev: ['multiline-block-like', 'multiline-expression'],
- // next: ['multiline-block-like', 'multiline-expression'],
- // },
- ],
-
- // TODO: re-enable these rules
- 'node/no-sync': 'off',
- 'node/no-unpublished-import': 'off',
- 'node/no-unpublished-require': 'off',
- 'jsdoc/match-description': 'off',
- 'jsdoc/require-description': 'off',
- 'jsdoc/require-jsdoc': 'off',
- 'jsdoc/require-param-description': 'off',
- 'jsdoc/require-param-type': 'off',
- 'jsdoc/require-returns-description': 'off',
- 'jsdoc/require-returns-type': 'off',
- 'jsdoc/require-returns': 'off',
- 'jsdoc/valid-types': 'off',
- },
overrides: [
+ /**
+ * == Modules ==
+ *
+ * The first two sections here, which cover module syntax, are mutually
+ * exclusive: the set of files covered between them may NOT overlap. This is
+ * because we do not allow a file to use two different styles for specifying
+ * imports and exports (however theoretically possible it may be).
+ */
+
{
- files: ['ui/**/*.js', 'test/lib/render-helpers.js', 'test/jest/*.js'],
- plugins: ['react'],
+ /**
+ * Modules (CommonJS module syntax)
+ *
+ * This is code that uses `require()` and `module.exports` to import and
+ * export other modules.
+ */
+ files: [
+ '.eslintrc.js',
+ '.eslintrc.*.js',
+ '.mocharc.js',
+ '*.config.js',
+ 'development/**/*.js',
+ 'test/e2e/**/*.js',
+ 'test/helpers/*.js',
+ 'test/lib/wait-until-called.js',
+ ],
+ extends: [
+ path.resolve(__dirname, '.eslintrc.base.js'),
+ path.resolve(__dirname, '.eslintrc.node.js'),
+ path.resolve(__dirname, '.eslintrc.babel.js'),
+ ],
+ parserOptions: {
+ sourceType: 'module',
+ },
+ rules: {
+ // This rule does not work with CommonJS modules. We will just have to
+ // trust that all of the files specified above are indeed modules.
+ 'import/unambiguous': 'off',
+ },
+ },
+ /**
+ * Modules (ES module syntax)
+ *
+ * This is code that explicitly uses `import`/`export` instead of
+ * `require`/`module.exports`.
+ */
+ {
+ files: [
+ 'app/**/*.js',
+ 'shared/**/*.js',
+ 'ui/**/*.js',
+ '**/*.test.js',
+ 'test/lib/**/*.js',
+ 'test/mocks/**/*.js',
+ 'test/jest/**/*.js',
+ 'test/stub/**/*.js',
+ 'test/unit-global/**/*.js',
+ ],
+ // TODO: Convert these files to modern JS
+ excludedFiles: ['test/lib/wait-until-called.js'],
+ extends: [
+ path.resolve(__dirname, '.eslintrc.base.js'),
+ path.resolve(__dirname, '.eslintrc.node.js'),
+ path.resolve(__dirname, '.eslintrc.babel.js'),
+ ],
+ parserOptions: {
+ sourceType: 'module',
+ },
+ },
+
+ /**
+ * == Everything else ==
+ *
+ * The sections from here on out may overlap with each other in various
+ * ways depending on their function.
+ */
+
+ /**
+ * React-specific code
+ *
+ * Code in this category contains JSX and hence needs to be run through the
+ * React plugin.
+ */
+ {
+ files: [
+ 'test/lib/render-helpers.js',
+ 'test/jest/rendering.js',
+ 'ui/**/*.js',
+ ],
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ plugins: ['react'],
rules: {
'react/no-unused-prop-types': 'error',
'react/no-unused-state': 'error',
@@ -139,74 +119,100 @@ module.exports = {
'react/default-props-match-prop-types': 'error',
'react/jsx-no-duplicate-props': 'error',
},
- },
- {
- files: ['test/e2e/**/*.spec.js'],
- extends: ['@metamask/eslint-config-mocha'],
- rules: {
- 'mocha/no-hooks-for-single-case': 'off',
- 'mocha/no-setup-in-describe': 'off',
+ settings: {
+ react: {
+ // If this is set to 'detect', ESLint will import React in order to
+ // find its version. Because we run ESLint in the build system under
+ // LavaMoat, this means that detecting the React version requires a
+ // LavaMoat policy for all of React, in the build system. That's a
+ // no-go, so we grab it from React's package.json.
+ version: reactVersion,
+ },
},
},
+ /**
+ * Mocha tests
+ *
+ * These are files that make use of globals and syntax introduced by the
+ * Mocha library.
+ */
{
- files: ['app/scripts/migrations/*.js', '*.stories.js'],
- rules: {
- 'import/no-anonymous-default-export': ['error', { allowObject: true }],
- },
- },
- {
- files: ['app/scripts/migrations/*.js'],
- rules: {
- 'node/global-require': 'off',
- },
- },
- {
- files: ['**/*.test.js'],
+ files: [
+ '**/*.test.js',
+ 'test/lib/wait-until-called.js',
+ 'test/e2e/**/*.spec.js',
+ ],
excludedFiles: [
- 'ui/**/*.test.js',
- 'ui/__mocks__/*.js',
- 'shared/**/*.test.js',
- 'development/**/*.test.js',
+ 'app/scripts/controllers/network/**/*.test.js',
+ 'app/scripts/controllers/permissions/**/*.test.js',
'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js',
'app/scripts/platforms/*.test.js',
- 'app/scripts/controllers/network/**/*.test.js',
- 'app/scripts/controllers/permissions/**/*.test.js',
+ 'development/**/*.test.js',
+ 'shared/**/*.test.js',
+ 'ui/**/*.test.js',
+ 'ui/__mocks__/*.js',
],
extends: ['@metamask/eslint-config-mocha'],
rules: {
+ // In Mocha tests, it is common to use `this` to store values or do
+ // things like force the test to fail.
+ '@babel/no-invalid-this': 'off',
'mocha/no-setup-in-describe': 'off',
},
},
+ /**
+ * Jest tests
+ *
+ * These are files that make use of globals and syntax introduced by the
+ * Jest library.
+ */
{
- files: ['**/__snapshots__/*.snap'],
- plugins: ['jest'],
+ files: [
+ '**/__snapshots__/*.snap',
+ 'app/scripts/controllers/network/**/*.test.js',
+ 'app/scripts/controllers/permissions/**/*.test.js',
+ 'app/scripts/lib/**/*.test.js',
+ 'app/scripts/migrations/*.test.js',
+ 'app/scripts/platforms/*.test.js',
+ 'development/**/*.test.js',
+ 'shared/**/*.test.js',
+ 'test/jest/*.js',
+ 'test/helpers/*.js',
+ 'ui/**/*.test.js',
+ 'ui/__mocks__/*.js',
+ ],
+ extends: ['@metamask/eslint-config-jest'],
+ parserOptions: {
+ sourceType: 'module',
+ },
rules: {
+ 'import/unambiguous': 'off',
+ 'import/named': 'off',
'jest/no-large-snapshots': [
'error',
{ maxSize: 50, inlineMaxSize: 50 },
],
- },
- },
- {
- files: [
- 'ui/**/*.test.js',
- 'ui/__mocks__/*.js',
- 'shared/**/*.test.js',
- 'development/**/*.test.js',
- 'app/scripts/lib/**/*.test.js',
- 'app/scripts/migrations/*.test.js',
- 'app/scripts/platforms/*.test.js',
- 'app/scripts/controllers/network/**/*.test.js',
- 'app/scripts/controllers/permissions/**/*.test.js',
- ],
- extends: ['@metamask/eslint-config-jest'],
- rules: {
'jest/no-restricted-matchers': 'off',
- 'import/unambiguous': 'off',
- 'import/named': 'off',
},
},
+ /**
+ * Migrations
+ */
+ {
+ files: ['app/scripts/migrations/*.js', '**/*.stories.js'],
+ rules: {
+ 'import/no-anonymous-default-export': ['error', { allowObject: true }],
+ },
+ },
+ /**
+ * Executables and related files
+ *
+ * These are files that run in a Node context. They are either designed to
+ * run as executables (in which case they will have a shebang at the top) or
+ * are dependencies of executables (in which case they may use
+ * `process.exit` to exit).
+ */
{
files: [
'development/**/*.js',
@@ -218,27 +224,9 @@ module.exports = {
'node/shebang': 'off',
},
},
- {
- files: [
- '.eslintrc.js',
- '.mocharc.js',
- 'babel.config.js',
- 'jest.config.js',
- 'nyc.config.js',
- 'stylelint.config.js',
- 'app/scripts/lockdown-run.js',
- 'app/scripts/lockdown-more.js',
- 'development/**/*.js',
- 'test/e2e/**/*.js',
- 'test/env.js',
- 'test/setup.js',
- 'test/helpers/protect-intrinsics-helpers.js',
- 'test/lib/wait-until-called.js',
- ],
- parserOptions: {
- sourceType: 'script',
- },
- },
+ /**
+ * Lockdown files
+ */
{
files: [
'app/scripts/lockdown-run.js',
@@ -251,19 +239,11 @@ module.exports = {
Compartment: 'readonly',
},
},
+ {
+ files: ['app/scripts/lockdown-run.js', 'app/scripts/lockdown-more.js'],
+ parserOptions: {
+ sourceType: 'script',
+ },
+ },
],
-
- settings: {
- jsdoc: {
- mode: 'typescript',
- },
- react: {
- // If this is set to 'detect', ESLint will import React in order to find
- // its version. Because we run ESLint in the build system under LavaMoat,
- // this means that detecting the React version requires a LavaMoat policy
- // for all of React, in the build system. That's a no-go, so we grab it
- // from React's package.json.
- version: reactVersion,
- },
- },
};
diff --git a/.eslintrc.jsdoc.js b/.eslintrc.jsdoc.js
new file mode 100644
index 000000000..862145853
--- /dev/null
+++ b/.eslintrc.jsdoc.js
@@ -0,0 +1,23 @@
+module.exports = {
+ // Note that jsdoc is already in the `plugins` array thanks to
+ // @metamask/eslint-config — this just extends the config there
+ rules: {
+ // Allow tag `jest-environment` to work around Jest bug
+ // See: https://github.com/facebook/jest/issues/7780
+ 'jsdoc/check-tag-names': ['error', { definedTags: ['jest-environment'] }],
+ 'jsdoc/match-description': 'off',
+ 'jsdoc/require-description': 'off',
+ 'jsdoc/require-jsdoc': 'off',
+ 'jsdoc/require-param-description': 'off',
+ 'jsdoc/require-param-type': 'off',
+ 'jsdoc/require-returns-description': 'off',
+ 'jsdoc/require-returns-type': 'off',
+ 'jsdoc/require-returns': 'off',
+ 'jsdoc/valid-types': 'off',
+ },
+ settings: {
+ jsdoc: {
+ mode: 'typescript',
+ },
+ },
+};
diff --git a/.eslintrc.node.js b/.eslintrc.node.js
new file mode 100644
index 000000000..78a12a346
--- /dev/null
+++ b/.eslintrc.node.js
@@ -0,0 +1,10 @@
+module.exports = {
+ extends: ['@metamask/eslint-config-nodejs'],
+ rules: {
+ 'node/no-process-env': 'off',
+ // TODO: re-enable these rules
+ 'node/no-sync': 'off',
+ 'node/no-unpublished-import': 'off',
+ 'node/no-unpublished-require': 'off',
+ },
+};
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index cb38aaf16..889bd9d1b 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -89,6 +89,7 @@ body:
- Trezor
- Keystone
- GridPlus Lattice1
+ - AirGap Vault
- Other (please elaborate in the "Additional Context" section)
- type: textarea
id: additional
diff --git a/.prettierignore b/.prettierignore
index 2e0417ca7..a98c312ad 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -10,3 +10,4 @@ app/vendor/**
.vscode/**
test/e2e/send-eth-with-private-key-test/**
*.scss
+development/chromereload.js
diff --git a/.storybook/3.COLORS.stories.mdx b/.storybook/3.COLORS.stories.mdx
new file mode 100644
index 000000000..b694a721a
--- /dev/null
+++ b/.storybook/3.COLORS.stories.mdx
@@ -0,0 +1,201 @@
+import { Meta } from '@storybook/addon-docs';
+import ActionaleMessage from '../ui/components/ui/actionable-message';
+import designTokenDiagramImage from './images/design.token.graphic.svg';
+
+
+
+# Color
+
+Color is used to express style and communicate meaning.
+
+
+
+
+
+## Design tokens
+
+We are importing design tokens as CSS variables from [@metamask/design-tokens](https://github.com/MetaMask/design-tokens) repo to help consolidate colors and enable theming across all MetaMask products.
+
+### Token tiers
+
+We follow a 3 tiered system for color design tokens and css variables.
+
+
+
+
+
+
+
+
+### **Brand colors** (tier 1)
+
+These colors **SHOULD NOT** be used in your styles directly. They are used as a reference for the [theme colors](#theme-colors-tier-2). Brand colors should just keep track of every color used in our app.
+
+#### Example of brand color css variables
+
+```css
+/** !!!DO NOT USE BRAND COLORS DIRECTLY IN YOUR CODE!!! */
+var(--brand-colors-white-white000)
+var(--brand-colors-white-white010)
+var(--brand-colors-grey-grey030)
+```
+
+### **Theme colors** (tier 2)
+
+Theme colors are color agnostic, semantically neutral and theme compatible design tokens that you can use in your code and styles. Please refer to the description of each token for it's intended purpose in [@metamask/design-tokens](https://github.com/MetaMask/design-tokens/blob/main/src/figma/tokens.json#L329-L554).
+
+#### Example of theme color css variables
+
+```css
+/** Backgrounds */
+var(--color-background-default)
+var(--color-background-alternative)
+
+/** Text */
+var(--color-text-default)
+var(--color-text-alternative)
+var(--color-text-muted)
+
+/** Icons */
+var(--color-icon-default)
+var(--color-icon-muted)
+
+/** Borders */
+var(--color-border-default)
+var(--color-border-muted)
+
+/** Overlays */
+var(--color-overlay-default)
+var(--color-overlay-inverse)
+
+/** User Actions */
+var(--color-primary-default)
+var(--color-primary-alternative)
+var(--color-primary-muted)
+var(--color-primary-inverse)
+var(--color-primary-disabled)
+
+var(--color-secondary-default)
+var(--color-secondary-alternative)
+var(--color-secondary-muted)
+var(--color-secondary-inverse)
+var(--color-secondary-disabled)
+
+/** States */
+/** Error */
+var(--color-error-default)
+var(--color-error-alternative)
+var(--color-error-muted)
+var(--color-error-inverse)
+var(--color-error-disabled)
+
+/** Warning */
+var(--color-warning-default)
+var(--color-warning-alternative)
+var(--color-warning-muted)
+var(--color-warning-inverse)
+var(--color-warning-disabled)
+
+/** Success */
+var(--color-success-default)
+var(--color-success-alternative)
+var(--color-success-muted)
+var(--color-success-inverse)
+var(--color-success-disabled)
+
+/** Info */
+var(--color-info-default)
+var(--color-info-alternative)
+var(--color-info-muted)
+var(--color-info-inverse)
+var(--color-info-disabled)
+```
+
+### **Component colors** (tier 3)
+
+Another level of abstraction is component tier colors that you can define at the top of your styles and use at the component specific level.
+
+```scss
+.button {
+ --color-background-primary: var(--color-primary-default);
+ --color-text-primary: var(--color-primary-inverse);
+ --color-border-primary: var(--color-primary-default);
+
+ --color-background-primary-hover: var(--color-primary-alternative);
+ --color-border-primary-hover: var(--color-primary-alternative);
+
+ .btn-primary {
+ background-color: var(--color-background-primary);
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border-primary);
+
+ &:hover {
+ background-color: var(--color-background-primary-hover);
+ border: 1px solid var(--color-border-primary-hover);
+ }
+
+ /** btn-primary css continued... */
+ }
+}
+```
+
+## Takeaways
+
+- Do not use static HEX values in your code. Use the [theme colors](#theme-colors-tier-2). If one does not exist for your use case ask the designer or [create an issue](https://github.com/MetaMask/metamask-extension/issues/new) and tag it with a `design-system` label.
+- Make sure the design token you are using is for it's intended purpose. Please refer to the description of each token in [@metamask/design-tokens](https://github.com/MetaMask/design-tokens/blob/main/src/figma/tokens.json#L329-L554).
+
+### ❌ Don't do this
+
+Don't use static hex values or brand color tokens in your code.
+
+```css
+/**
+* Don't do this
+* Static hex values create inconsistency and will break UI when using dark mode
+**/
+.card {
+ background-color: #ffffff;
+ color: #24272a;
+}
+
+/**
+* Don't do this
+* Not theme compatible and will break UI when using dark theme
+**/
+.card {
+ background-color: var(--brand-colors-white-white000);
+ color: var(--brand-colors-grey-grey800);
+}
+```
+
+### ✅ Do this
+
+Do use component tiered and [theme colors](#theme-colors-tier-2) in your styles and code
+
+```css
+.card {
+ --color-background: var(--color-background-default);
+ --color-text: var(--color-text-default);
+
+ background-color: var(--color-background);
+ color: var(--color-text);
+}
+```
+
+
+
+## References
+
+- [@metamask/design-tokens](https://github.com/MetaMask/design-tokens)
+- [Figma brand colors library](https://www.figma.com/file/cBAUPFMnbv6tHR1J8KvBI2/Brand-Colors?node-id=0%3A1) (internal use only)
+- [Figma theme colors library](https://www.figma.com/file/kdFzEC7xzSNw7cXteqgzDW/Light-Theme-Colors?node-id=0%3A1) (internal use only)
+- [Figma dark theme colors library](https://www.figma.com/file/rLKsoqpjyoKauYnFDcBIbO/Dark-Theme-Colors?node-id=0%3A1) (internal use only)
diff --git a/.storybook/images/design.token.graphic.svg b/.storybook/images/design.token.graphic.svg
new file mode 100644
index 000000000..ad64afba9
--- /dev/null
+++ b/.storybook/images/design.token.graphic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.storybook/main.js b/.storybook/main.js
index 30c216825..5ce2f32d7 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -14,6 +14,7 @@ module.exports = {
'@storybook/addon-a11y',
'@storybook/addon-knobs',
'./i18n-party-addon/register.js',
+ 'storybook-dark-mode',
],
// Uses babel.config.js settings and prevents "Missing class properties transform" error
babel: async (options) => ({ overrides: options.overrides }),
diff --git a/.storybook/preview.js b/.storybook/preview.js
index 9e6fe2206..517e7820a 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { addDecorator, addParameters } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Provider } from 'react-redux';
@@ -13,13 +13,14 @@ import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { _setBackgroundConnection } from '../ui/store/actions';
import MetaMaskStorybookTheme from './metamask-storybook-theme';
+import addons from '@storybook/addons';
addParameters({
backgrounds: {
- default: 'light',
+ default: 'default',
values: [
- { name: 'light', value: '#FFFFFF' },
- { name: 'dark', value: '#333333' },
+ { name: 'default', value: 'var(--color-background-default)' },
+ { name: 'alternative', value: 'var(--color-background-alternative)' },
],
},
docs: {
@@ -27,7 +28,13 @@ addParameters({
},
options: {
storySort: {
- order: ['Getting Started', 'Components', ['UI', 'App'], 'Pages'],
+ order: [
+ 'Getting Started',
+ 'Design Tokens',
+ 'Components',
+ ['UI', 'App'],
+ 'Pages',
+ ],
},
},
});
@@ -66,8 +73,29 @@ const proxiedBackground = new Proxy(
_setBackgroundConnection(proxiedBackground);
const metamaskDecorator = (story, context) => {
+ const [isDark, setDark] = useState(false);
+ const channel = addons.getChannel();
const currentLocale = context.globals.locale;
const current = allLocales[currentLocale];
+
+ useEffect(() => {
+ channel.on('DARK_MODE', setDark);
+ return () => channel.off('DARK_MODE', setDark);
+ }, [channel, setDark]);
+
+ useEffect(() => {
+ const currentTheme = document.documentElement.getAttribute('data-theme');
+
+ if (!currentTheme)
+ document.documentElement.setAttribute('data-theme', 'light');
+
+ if (currentTheme === 'light' && isDark) {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ } else if (currentTheme === 'dark' && !isDark) {
+ document.documentElement.setAttribute('data-theme', 'light');
+ }
+ }, [isDark]);
+
return (
diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json
index 51ea3e653..8d9e27e13 100644
--- a/app/_locales/am/messages.json
+++ b/app/_locales/am/messages.json
@@ -750,9 +750,6 @@
"restore": {
"message": "እነበረበት መልስ"
},
- "restoreAccountWithSeed": {
- "message": "መለያዎን በዘር ሐረግ ወደነበረበት ይመልሱ"
- },
"revealSeedWords": {
"message": "የዘር ቃላትን ይግለጹ"
},
diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json
index 0a93bbd3e..d3e78ae7b 100644
--- a/app/_locales/ar/messages.json
+++ b/app/_locales/ar/messages.json
@@ -766,9 +766,6 @@
"restore": {
"message": "استعادة"
},
- "restoreAccountWithSeed": {
- "message": "قم باستعادة حسابك بواسطة عبارة الأمان"
- },
"revealSeedWords": {
"message": "كشف كلمات عبارات الأمان"
},
diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json
index 35f765175..df8c830ae 100644
--- a/app/_locales/bg/messages.json
+++ b/app/_locales/bg/messages.json
@@ -761,9 +761,6 @@
"restore": {
"message": "Възстановяване"
},
- "restoreAccountWithSeed": {
- "message": "Възстановете акаунта си с фраза зародиш"
- },
"revealSeedWords": {
"message": "Разкрий думите зародиш"
},
diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json
index 69577c0e7..7290dab16 100644
--- a/app/_locales/bn/messages.json
+++ b/app/_locales/bn/messages.json
@@ -765,9 +765,6 @@
"restore": {
"message": "পুনরুদ্ধার করুন"
},
- "restoreAccountWithSeed": {
- "message": "সীড ফ্রেজ দিয়ে আপনার অ্যাকাউন্ট রিস্টোর করুন"
- },
"revealSeedWords": {
"message": "সীড শব্দগুলি প্রকাশ করুন"
},
diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json
index 03ff652ce..ce8f14237 100644
--- a/app/_locales/ca/messages.json
+++ b/app/_locales/ca/messages.json
@@ -743,9 +743,6 @@
"restore": {
"message": "Restaura"
},
- "restoreAccountWithSeed": {
- "message": "Restaura el teu compte amb Frase de Recuperació"
- },
"revealSeedWords": {
"message": "Revelar Paraules de Recuperació"
},
diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json
index 709e63bcc..f6f1e92d6 100644
--- a/app/_locales/da/messages.json
+++ b/app/_locales/da/messages.json
@@ -746,9 +746,6 @@
"restore": {
"message": "Gendan"
},
- "restoreAccountWithSeed": {
- "message": "Gendan din konto med Seed-sætning"
- },
"revealSeedWords": {
"message": "Vis Seedord"
},
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index 5549b95d7..1417de452 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -1279,19 +1279,12 @@
"importAccountError": {
"message": "Fehler beim Importieren des Kontos."
},
- "importAccountLinkText": {
- "message": "mit einer Geheime Wiederherstellungsphrase importieren"
- },
"importAccountMsg": {
"message": " Importierte Accounts werden nicht mit der Seed-Wörterfolge deines ursprünglichen MetaMask Accounts verknüpft. Erfahre mehr über importierte Accounts."
},
"importAccountSeedPhrase": {
"message": "Ein Konto mit einem Seed-Schlüssel importieren"
},
- "importAccountText": {
- "message": "oder $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Geben Sie die Geheime Wiederherstellungsphrase (alias Seed Phrase) ein, die Sie beim Erstellen Ihrer Wallet erhalten haben. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Konto $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Sammelobjekt wurde nicht hinzugefügt, weil: $1"
- },
"newCollectibleAddedMessage": {
"message": "Sammelobjekt wurde erfolgreich hinzugefügt!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Wiederherstellen"
},
- "restoreAccountWithSeed": {
- "message": "Ihr Konto mit mnemonischer Phrase wiederherstellen"
- },
"restoreWalletPreferences": {
"message": "$1 hat ein Backup Ihrer Daten gefunden. Möchten Sie die Präferenzen Ihrer Wallet wiederherstellen?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Nur das erste Konto auf dieser Wallet wird automatisch geladen. Wenn Sie nach Abschluss dieses Vorgangs weitere Konten hinzufügen möchten, klicken Sie auf das Dropdown-Menü und wählen Sie dann Konto erstellen."
},
- "secretPhraseWarning": {
- "message": "Wenn Sie eine andere geheime Wiederherstellungsphrase verwenden, werden Ihre aktuelle Wallet, Ihre Konten und Vermögenswerte dauerhaft aus dieser App entfernt. Diese Aktion kann nicht rückgängig gemacht werden."
- },
"secretRecoveryPhrase": {
"message": "Geheime Wiederherstellungsphrase"
},
diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json
index 387bf6031..b66f85652 100644
--- a/app/_locales/el/messages.json
+++ b/app/_locales/el/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Σφάλμα εισαγωγής λογαριασμού."
},
- "importAccountLinkText": {
- "message": "εισαγωγή χρησιμοποιώντας τη Μυστική Φράση Ανάκτησης"
- },
"importAccountMsg": {
"message": "Οι λογαριασμοί που εισάγονται δεν θα συσχετιστούν με τη Μυστική Φράση Ανάκτησης του λογαριασμού σας MetaTask που δημιουργήθηκε αρχικά. Μάθετε περισσότερα για τους εισηγμένους λογαριασμούς"
},
"importAccountSeedPhrase": {
"message": "Εισαγωγή λογαριασμού με Μυστική Φράση Ανάκτησης"
},
- "importAccountText": {
- "message": "ή $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Εισάγετε τη Μυστική Φράση Ανάκτησης (δλδ Seed Phrase) που σας δόθηκε όταν δημιουργήσατε το πορτοφόλι σας. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Λογαριασμός $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Το Collectible δεν προστέθηκε επειδή: $1"
- },
"newCollectibleAddedMessage": {
"message": "Το Collectible προστέθηκε με επιτυχία!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Επαναφορά"
},
- "restoreAccountWithSeed": {
- "message": "Επαναφέρετε τον Λογαριασμό σας με Φράση Επαναφοράς"
- },
"restoreWalletPreferences": {
"message": "Βρέθηκε ένα αντίγραφο ασφαλείας των δεδομένων σας από το $1. Θα θέλατε να επαναφέρετε τις προτιμήσεις του πορτοφολιού σας;",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Μόνο ο πρώτος λογαριασμός σε αυτό το πορτοφόλι θα φορτώσει αυτόματα. Μετά την ολοκλήρωση αυτής της διαδικασίας, για να προσθέσετε επιπλέον λογαριασμούς, κάντε κλικ στο αναπτυσσόμενο μενού και, στη συνέχεια, επιλέξτε Δημιουργία Λογαριασμού."
},
- "secretPhraseWarning": {
- "message": "Αν κάνετε επαναφορά χρησιμοποιώντας μια άλλη Μυστική Φράση Ανάκτησης, το τρέχον πορτοφόλι, οι λογαριασμοί και τα περιουσιακά στοιχεία σας θα αφαιρεθούν από αυτή την εφαρμογή μόνιμα. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί."
- },
"secretRecoveryPhrase": {
"message": "Μυστική Φράση Ανάκτησης"
},
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index ba8a7c370..ebe1cbc07 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -42,7 +42,7 @@
"message": "QR-based HW Wallet"
},
"QRHardwareWalletSteps2Description": {
- "message": "AirGap Vault & Ngrave (Coming Soon)"
+ "message": "Ngrave (Coming Soon)"
},
"about": {
"message": "About"
@@ -98,6 +98,9 @@
"addANetwork": {
"message": "Add a network"
},
+ "addANetworkManually": {
+ "message": "Add a network manually"
+ },
"addANickname": {
"message": "Add a nickname"
},
@@ -137,6 +140,9 @@
"addFriendsAndAddresses": {
"message": "Add friends and addresses you trust"
},
+ "addFromAListOfPopularNetworks": {
+ "message": "Add from a list of popular networks or add a network manually. Only interact with the entities you trust."
+ },
"addMemo": {
"message": "Add memo"
},
@@ -191,6 +197,12 @@
"aggregatorFeeCost": {
"message": "Aggregator network fee"
},
+ "airgapVault": {
+ "message": "AirGap Vault"
+ },
+ "airgapVaultTutorial": {
+ "message": " (Tutorials)"
+ },
"alertDisableTooltip": {
"message": "This can be changed in \"Settings > Alerts\""
},
@@ -400,6 +412,13 @@
"message": "Transak supports debit card and bank transfers (depending on location) in 59+ countries. $1 deposits into your MetaMask account.",
"description": "$1 represents the cypto symbol to be purchased"
},
+ "buyEth": {
+ "message": "Buy ETH"
+ },
+ "buyOther": {
+ "message": "Buy $1 or deposit from another account.",
+ "description": "$1 is a token symbol"
+ },
"buyWithWyre": {
"message": "Buy ETH with Wyre"
},
@@ -467,6 +486,9 @@
"close": {
"message": "Close"
},
+ "collectibleAddFailedMessage": {
+ "message": "NFT can’t be added as the ownership details do not match. Make sure you have entered correct information."
+ },
"collectibleAddressError": {
"message": "This token is an NFT. Add on the $1",
"description": "$1 is a clickable link with text defined by the 'importNFTPage' key"
@@ -691,6 +713,9 @@
"customGasSubTitle": {
"message": "Increasing fee may decrease processing times, but it is not guaranteed."
},
+ "customNetworks": {
+ "message": "Custom networks"
+ },
"customSpendLimit": {
"message": "Custom Spend Limit"
},
@@ -989,7 +1014,7 @@
"message": "Use OpenSea's API to fetch NFT data. NFT auto-detection relies on OpenSea's API, and will not be available when this is turned off."
},
"enableSmartTransactions": {
- "message": "Enable smart transactions"
+ "message": "Enable Smart Transactions"
},
"enableToken": {
"message": "enable $1",
@@ -1206,6 +1231,9 @@
"forgetDevice": {
"message": "Forget this device"
},
+ "forgotPassword": {
+ "message": "Forgot password?"
+ },
"from": {
"message": "From"
},
@@ -1334,6 +1362,9 @@
"goerli": {
"message": "Goerli Test Network"
},
+ "gotIt": {
+ "message": "Got it!"
+ },
"grantedToWithColon": {
"message": "Granted to:"
},
@@ -1408,19 +1439,12 @@
"importAccountError": {
"message": "Error importing account."
},
- "importAccountLinkText": {
- "message": "import using Secret Recovery Phrase"
- },
"importAccountMsg": {
"message": "Imported accounts will not be associated with your originally created MetaMask account Secret Recovery Phrase. Learn more about imported accounts"
},
"importAccountSeedPhrase": {
"message": "Import a wallet with Secret Recovery Phrase"
},
- "importAccountText": {
- "message": "or $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Enter your Secret Recovery Phrase (aka Seed Phrase) that you were given when you created your wallet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1478,6 +1502,10 @@
"insufficientBalance": {
"message": "Insufficient balance."
},
+ "insufficientCurrency": {
+ "message": "You do not have enough $1 in your account to pay for transaction fees on $2 network.",
+ "description": "$1 is currency, $2 is network"
+ },
"insufficientFunds": {
"message": "Insufficient funds."
},
@@ -1677,6 +1705,10 @@
"lockTimeTooGreat": {
"message": "Lock time is too great"
},
+ "logo": {
+ "message": "$1 logo",
+ "description": "$1 is the name of the ticker"
+ },
"low": {
"message": "Low"
},
@@ -1815,6 +1847,12 @@
"missingNFT": {
"message": "Don't see your NFT?"
},
+ "missingSetting": {
+ "message": "Can't find a setting?"
+ },
+ "missingSettingRequest": {
+ "message": "Request here"
+ },
"missingToken": {
"message": "Don't see your token?"
},
@@ -1926,9 +1964,6 @@
"message": "Account $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Collectible was not added because: $1"
- },
"newCollectibleAddedMessage": {
"message": "Collectible was successfully added!"
},
@@ -2199,6 +2234,9 @@
"onlyConnectTrust": {
"message": "Only connect with sites you trust."
},
+ "onlyInteractWith": {
+ "message": "Only interact with entities you trust."
+ },
"openFullScreenForLedgerWebHid": {
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."
@@ -2215,6 +2253,9 @@
"or": {
"message": "or"
},
+ "orDeposit": {
+ "message": "or deposit from another account."
+ },
"origin": {
"message": "Origin"
},
@@ -2356,6 +2397,12 @@
"queued": {
"message": "Queued"
},
+ "reAddAccounts": {
+ "message": "re-add any other accounts"
+ },
+ "reAdded": {
+ "message": "re-added"
+ },
"readdToken": {
"message": "You can add this token back in the future by going to “Import token” in your accounts options menu."
},
@@ -2455,12 +2502,21 @@
"resetAccountDescription": {
"message": "Resetting your account will clear your transaction history. This will not change the balances in your accounts or require you to re-enter your Secret Recovery Phrase."
},
+ "resetWallet": {
+ "message": "Reset Wallet"
+ },
+ "resetWalletSubHeader": {
+ "message": "MetaMask does not keep a copy of your password. If you’re having trouble unlocking your account, you will need to reset your wallet. You can do this by providing the Secret Recovery Phrase you used when you set up your wallet."
+ },
+ "resetWalletUsingSRP": {
+ "message": "This action will delete your current wallet and Secret Recovery Phrase from this device, along with the list of accounts you’ve curated. After resetting with a Secret Recovery Phrase, you’ll see a list of accounts based on the Secret Recovery Phrase you use to reset. This new list will automatically include accounts that have a balance. You’ll also be able to $1 created previously. Custom accounts that you’ve imported will need to be $2, and any custom tokens you’ve added to an account will need to be $3 as well."
+ },
+ "resetWalletWarning": {
+ "message": "Make sure you’re using the correct Secret Recovery Phrase before proceeding. You will not be able to undo this."
+ },
"restore": {
"message": "Restore"
},
- "restoreAccountWithSeed": {
- "message": "Restore your Account with Secret Recovery Phrase"
- },
"restoreWalletPreferences": {
"message": "A backup of your data from $1 has been found. Would you like to restore your wallet preferences?",
"description": "$1 is the date at which the data was backed up"
@@ -2516,6 +2572,9 @@
"searchResults": {
"message": "Search Results"
},
+ "searchSettings": {
+ "message": "Search in settings"
+ },
"searchTokens": {
"message": "Search Tokens"
},
@@ -2528,9 +2587,6 @@
"secretPhrase": {
"message": "Only the first account on this wallet will auto load. After completing this process, to add additional accounts, click the drop down menu, then select Create Account."
},
- "secretPhraseWarning": {
- "message": "If you restore using another Secret Recovery Phrase, your current wallet, accounts and assets will be removed from this app permanently. This action cannot be undone."
- },
"secretRecoveryPhrase": {
"message": "Secret Recovery Phrase"
},
@@ -2553,22 +2609,22 @@
"message": "Secure my wallet (recommended)"
},
"seedPhraseIntroSidebarBulletFour": {
- "message": "Write down and store in multiple secret places."
+ "message": "Write down and store in multiple secret places"
},
"seedPhraseIntroSidebarBulletOne": {
"message": "Save in a password manager"
},
"seedPhraseIntroSidebarBulletThree": {
- "message": "Store in a safe-deposit box."
+ "message": "Store in a safe deposit box"
},
"seedPhraseIntroSidebarBulletTwo": {
- "message": "Store in a bank vault."
+ "message": "Store in a bank vault"
},
"seedPhraseIntroSidebarCopyOne": {
"message": "Your Secret Recovery Phrase is a 12-word phrase that is the “master key” to your wallet and your funds"
},
"seedPhraseIntroSidebarCopyThree": {
- "message": "If someone asks for your recovery phrase they are likely trying to scam you and steal your wallet funds"
+ "message": "If someone asks for your recovery phrase they are likely trying to scam you and steal your wallet funds."
},
"seedPhraseIntroSidebarCopyTwo": {
"message": "Never, ever share your Secret Recovery Phrase, not even with MetaMask!"
@@ -2668,6 +2724,9 @@
"settings": {
"message": "Settings"
},
+ "settingsSearchMatchingNotFound": {
+ "message": "No matching results found"
+ },
"show": {
"message": "Show"
},
@@ -2890,19 +2949,19 @@
"message": "Store this phrase in a password manager like 1Password."
},
"stxAreHere": {
- "message": "Smart transactions are here!"
+ "message": "Smart Transactions are here!"
},
"stxBenefit1": {
- "message": "Decrease transaction costs"
+ "message": "Minimize transaction costs"
},
"stxBenefit2": {
- "message": "Reduce failures & minimize costs"
+ "message": "Reduce transaction failures"
},
"stxBenefit3": {
- "message": "Protect from front-running"
+ "message": "Eliminate stuck transactions"
},
"stxBenefit4": {
- "message": "Eliminate stuck transactions"
+ "message": "Prevent front-running"
},
"stxCancelled": {
"message": "Swap would have failed"
@@ -2914,7 +2973,7 @@
"message": "Try your swap again. We’ll be here to protect you against similar risks next time."
},
"stxDescription": {
- "message": "Smart transactions use MetaMask smart contracts to simulate transactions before submitting in order to..."
+ "message": "MetaMask Swaps just got a whole lot smarter! Enabling Smart Transactions will allow MetaMask to programmatically optimize your Swap to help:"
},
"stxFailure": {
"message": "Swap failed"
@@ -2936,7 +2995,7 @@
"message": "Privately submitting the Swap..."
},
"stxSubDescription": {
- "message": "Enabling allows MetaMask to simulate transactions, proactively cancel bad transactions and sign MetaMask Swaps transactions for you."
+ "message": "* Smart Transactions will attempt to submit your transaction privately, multiple times. If all attempts fail, the transaction will be broadcast publicly to ensure your Swap successfully goes through."
},
"stxSuccess": {
"message": "Swap complete!"
@@ -3606,6 +3665,9 @@
"message": "Sending collectible (ERC-721) tokens is not currently supported",
"description": "This is an error message we show the user if they attempt to send a collectible asset type, for which currently don't support sending"
},
+ "unverifiedContractAddressMessage": {
+ "message": "We cannot verify this contract. Make sure you trust this address."
+ },
"updatedWithDate": {
"message": "Updated $1"
},
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index cefb30eaa..a08ff6118 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -826,19 +826,12 @@
"importAccount": {
"message": "Importar cuenta"
},
- "importAccountLinkText": {
- "message": "importar con la frase secreta de recuperación"
- },
"importAccountMsg": {
"message": " Las cuentas importadas no se asociarán con la frase secreta de recuperación de la cuenta original de MetaMask. Más información sobre las cuentas importadas "
},
"importAccountSeedPhrase": {
"message": "Importar una cuenta con la frase secreta de recuperación"
},
- "importAccountText": {
- "message": "o $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importTokenQuestion": {
"message": "¿Desea importar el token?"
},
@@ -1421,9 +1414,6 @@
"restore": {
"message": "Restaurar"
},
- "restoreAccountWithSeed": {
- "message": "Restaurar la cuenta con la frase secreta de recuperación"
- },
"restoreWalletPreferences": {
"message": "Se encontró una copia de seguridad de los datos de $1. ¿Desea restaurar las preferencias de cartera?",
"description": "$1 is the date at which the data was backed up"
diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json
index b1d29d4a9..a154558d3 100644
--- a/app/_locales/es_419/messages.json
+++ b/app/_locales/es_419/messages.json
@@ -1322,19 +1322,12 @@
"importAccountError": {
"message": "Error al importar la cuenta."
},
- "importAccountLinkText": {
- "message": "importar con la frase secreta de recuperación"
- },
"importAccountMsg": {
"message": "Las cuentas importadas no se asociarán con la frase secreta de recuperación de la cuenta original de MetaMask. Aprenda más sobre las cuentas importadas"
},
"importAccountSeedPhrase": {
"message": "Importar una cartera con la frase secreta de recuperación"
},
- "importAccountText": {
- "message": "o $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Ingrese su frase secreta de recuperación (también conocida como Frase Semilla) que recibió al crear su cartera. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1794,9 +1787,6 @@
"message": "Cuenta $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "No se añadió el coleccionable porque: $1"
- },
"newCollectibleAddedMessage": {
"message": "¡El coleccionable fue añadido con éxito!"
},
@@ -2278,9 +2268,6 @@
"restore": {
"message": "Restaurar"
},
- "restoreAccountWithSeed": {
- "message": "Restaurar la cuenta con la frase secreta de recuperación"
- },
"restoreWalletPreferences": {
"message": "Se encontró una copia de seguridad de los datos de $1. ¿Desea restaurar las preferencias de cartera?",
"description": "$1 is the date at which the data was backed up"
@@ -2346,10 +2333,7 @@
"message": "ADVERTENCIA: No revele su frase de respaldo. Cualquier persona que tenga esta frase puede robarle los ethers."
},
"secretPhrase": {
- "message": "Solo la primera cuenta de esta cartera se cargará automáticamente. Después de llevar a cabo este proceso, para agregar cuentas adicionales haga clic en el menú desplegable y luego seleccione Crear cuenta."
- },
- "secretPhraseWarning": {
- "message": "Si restablece utilizando otra frase secreta de recuperación, su cartera actual, sus cuentas y sus activos se eliminarán de esta aplicación de forma permanente. Esta acción es irreversible."
+ "message": "Ingrese su frase secreta aquí para restaurar su bóveda."
},
"secretRecoveryPhrase": {
"message": "Frase secreta de recuperación"
diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json
index b349b34b2..8bf5aa8ac 100644
--- a/app/_locales/et/messages.json
+++ b/app/_locales/et/messages.json
@@ -755,9 +755,6 @@
"restore": {
"message": "Taasta"
},
- "restoreAccountWithSeed": {
- "message": "Taastage konto seemnefraasi abil"
- },
"revealSeedWords": {
"message": "Kuva seemnesõnu"
},
diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json
index eb43442d2..32b7c8e48 100644
--- a/app/_locales/fa/messages.json
+++ b/app/_locales/fa/messages.json
@@ -765,9 +765,6 @@
"restore": {
"message": "بازیابی"
},
- "restoreAccountWithSeed": {
- "message": "حساب تان را با عبارت بازیاب، بازیابی کنید"
- },
"revealSeedWords": {
"message": "کلمات بازیاب را آشکار کنید"
},
diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json
index d00a8ff3e..108165b57 100644
--- a/app/_locales/fi/messages.json
+++ b/app/_locales/fi/messages.json
@@ -762,9 +762,6 @@
"restore": {
"message": "Palauta"
},
- "restoreAccountWithSeed": {
- "message": "Palauta tilisi käyttäen salaustekstiä (seed phrase)"
- },
"revealSeedWords": {
"message": "Paljasta salaussanat"
},
diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json
index 06999930d..64695058e 100644
--- a/app/_locales/fil/messages.json
+++ b/app/_locales/fil/messages.json
@@ -689,9 +689,6 @@
"restore": {
"message": "Ipanumbalik"
},
- "restoreAccountWithSeed": {
- "message": "I-restore ang iyong Account gamit ang Seed Phrase"
- },
"revealSeedWords": {
"message": "Ipakita ang Seed Words"
},
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index b1bc21ee4..a311e6ca5 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Erreur d’importation de compte."
},
- "importAccountLinkText": {
- "message": "importer en utilisant la phrase secrète de récupération"
- },
"importAccountMsg": {
"message": "Les comptes importés ne seront pas associés à la phrase secrète de récupération que vous avez créée au départ dans MetaMask. En savoir plus sur les comptes importés"
},
"importAccountSeedPhrase": {
"message": "Importez un compte avec une phrase mnémotechnique"
},
- "importAccountText": {
- "message": "ou $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Saisissez la phrase secrète de récupération (aussi appelée « phrase mnémonique » ou « seed ») qui vous a été attribuée lors de la création de votre portefeuille. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Compte $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Le collectible n’a pas été ajouté, car : $1"
- },
"newCollectibleAddedMessage": {
"message": "Le collectible a été ajouté avec succès !"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Restaurer"
},
- "restoreAccountWithSeed": {
- "message": "Restaurer votre compte avec une phrase Seed."
- },
"restoreWalletPreferences": {
"message": "Une sauvegarde de vos données de $1 a été trouvée. Voulez-vous restaurer vos préférences de portefeuille ?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Seul le premier compte de ce portefeuille sera chargé automatiquement. Après avoir terminé ce processus, pour ajouter des comptes supplémentaires, cliquez sur le menu déroulant, puis sélectionnez Créer un compte."
},
- "secretPhraseWarning": {
- "message": "Si vous effectuez une restauration à l’aide d’une autre phrase secrète de récupération, votre portefeuille, vos comptes et vos actifs actuels seront définitivement supprimés de cette application. Cette action est irréversible."
- },
"secretRecoveryPhrase": {
"message": "Phrase secrète de récupération"
},
diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json
index 70a04be83..8cfe2edbd 100644
--- a/app/_locales/he/messages.json
+++ b/app/_locales/he/messages.json
@@ -762,9 +762,6 @@
"restore": {
"message": "שחזר"
},
- "restoreAccountWithSeed": {
- "message": "שחזר את חשבונך באמצעות צירוף הגרעין"
- },
"revealSeedWords": {
"message": "גלה מילות Seed"
},
diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json
index 0853cd02b..626d1ae25 100644
--- a/app/_locales/hi/messages.json
+++ b/app/_locales/hi/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "खाता आयात करने में त्रुटि।"
},
- "importAccountLinkText": {
- "message": "गुप्त रिकवरी फ्रेज का उपयोग करके आयात करें"
- },
"importAccountMsg": {
"message": "आयातित खाते आपके मूल रूप से बनाए गए MetaMask खाते के गुप्त रिकवरी फ्रेज से संबद्ध नहीं होंगे। आयातित खातों के बारे में अधिक जानें"
},
"importAccountSeedPhrase": {
"message": "गुप्त रिकवरी फ्रेज के साथ एक खाता आयात करें"
},
- "importAccountText": {
- "message": "या $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "अपना सीक्रेट रिकवरी फ्रेज (उर्फ सीड फ्रेज) दर्ज करें जो आपको अपना वॉलेट बनाने पर दिया गया था। $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "खाता $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "संग्रहणीय नहीं जोड़ा गया था क्योंकि: $1"
- },
"newCollectibleAddedMessage": {
"message": "संग्रहणीय सफलतापूर्वक जोड़ा गया!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "पुनर्स्थापित करें"
},
- "restoreAccountWithSeed": {
- "message": "गुप्त रिकवरी फ्रेज के साथ अपने खाते को पुनर्स्थापित करें"
- },
"restoreWalletPreferences": {
"message": "$1 से आपके डेटा का बैकअप मिला है। क्या आप अपनी वॉलेट वरीयताओं को पुनर्स्थापित करना चाहते हैं?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "इस वॉलेट पर केवल पहला खाता स्वतः लोड होगा। इस प्रक्रिया को पूरा करने के बाद, अतिरिक्त खाते जोड़ने के लिए, ड्रॉप डाउन मेन्यू पर क्लिक करें, फिर खाता बनाएं चुनें।"
},
- "secretPhraseWarning": {
- "message": "यदि आप किसी दूसरे सीक्रेट रिकवरी फ्रेज का उपयोग कर पुनर्स्थापित करते हैं, तो इस ऐप से आपके वर्तमान वॉलेट, अकाउंट, और संपति स्थायी रूप से हटा दिये जाएंगे। यह क्रिया पूर्ववत नहीं की जा सकती।"
- },
"secretRecoveryPhrase": {
"message": "सीक्रेट रिकवरी फ्रेज"
},
diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json
index 1de58c64d..eb3ffefb1 100644
--- a/app/_locales/hr/messages.json
+++ b/app/_locales/hr/messages.json
@@ -758,9 +758,6 @@
"restore": {
"message": "Vrati"
},
- "restoreAccountWithSeed": {
- "message": "Obnovite svoj račun početnom rečenicom"
- },
"revealSeedWords": {
"message": "Otkrij početne riječi"
},
diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json
index dae10bef3..44409dd45 100644
--- a/app/_locales/ht/messages.json
+++ b/app/_locales/ht/messages.json
@@ -479,9 +479,6 @@
"restore": {
"message": "Retabli"
},
- "restoreAccountWithSeed": {
- "message": "Retabli kont ou avèk yo Seed Fraz"
- },
"revealSeedWords": {
"message": "Revele Seed Mo Yo"
},
diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json
index 764e55689..ddbde0797 100644
--- a/app/_locales/hu/messages.json
+++ b/app/_locales/hu/messages.json
@@ -758,9 +758,6 @@
"restore": {
"message": "Visszaállítás"
},
- "restoreAccountWithSeed": {
- "message": "Fiók helyreállítása a seed mondat segítségével"
- },
"revealSeedWords": {
"message": "Seed szavak megjelenítése"
},
diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json
index 84418a062..bae413007 100644
--- a/app/_locales/id/messages.json
+++ b/app/_locales/id/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Galat saat mengimpor akun."
},
- "importAccountLinkText": {
- "message": "impor menggunakan Frasa Pemulihan Rahasia"
- },
"importAccountMsg": {
"message": "Akun yang diimpor tidak akan dikaitkan dengan Frasa Pemulihan Rahasia akun MetaMask yang asli dibuat. Pelajari selengkapnya tentang akun yang diimpor"
},
"importAccountSeedPhrase": {
"message": "Impor dompet dengan Frasa Pemulihan Rahasia"
},
- "importAccountText": {
- "message": "atau $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Masukkan Frasa Pemulihan Rahasia Anda (alias Frasa Benih) yang diberikan saat Anda membuat dompet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Akun $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Koleksi tidak ditambahkan karena: $1"
- },
"newCollectibleAddedMessage": {
"message": "Koleksi berhasil ditambahkan!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Pulihkan"
},
- "restoreAccountWithSeed": {
- "message": "Pulihkan Akun dengan Frasa Pemulihan Rahasia"
- },
"restoreWalletPreferences": {
"message": "Cadangan data Anda dari $1 telah ditemukan. Pulihkan preferensi dompet Anda?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Hanya akun pertama di dompet ini yang akan dimuat secara otomatis. Setelah proses ini selesai, untuk menambahkan akun tambahan, klik menu drop down, lalu pilih Buat Akun."
},
- "secretPhraseWarning": {
- "message": "Jika Anda memulihkan menggunakan Frasa Pemulihan Rahasia lainnya, dompet, akun, dan aset Anda saat ini akan dihapus dari aplikasi ini secara permanen. Tindakan ini tidak dapat dibatalkan."
- },
"secretRecoveryPhrase": {
"message": "Frasa Pemulihan Rahasia"
},
diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json
index 3ebfc6e95..b686b183b 100644
--- a/app/_locales/it/messages.json
+++ b/app/_locales/it/messages.json
@@ -1152,9 +1152,6 @@
"restore": {
"message": "Ripristina"
},
- "restoreAccountWithSeed": {
- "message": "Ripristina Account con la Frase Seed"
- },
"restoreWalletPreferences": {
"message": "È stato trovato un backup dei tuoi dati da $1. Vuoi ripristinare le preferenze del portafoglio?",
"description": "$1 is the date at which the data was backed up"
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 88ff32476..e544f3a6b 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "アカウントのインポート中にエラーが発生しました。"
},
- "importAccountLinkText": {
- "message": "シークレットリカバリーフレーズを使用してインポート"
- },
"importAccountMsg": {
"message": " インポートされたアカウントは、最初に作成したMetaMaskアカウントのシークレットリカバリーフレーズと関連付けられません。インポートされたアカウントの詳細を表示"
},
"importAccountSeedPhrase": {
"message": "シークレットリカバリーフレーズを使用してウォレットをインポート"
},
- "importAccountText": {
- "message": "または$1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "ウォレットの作成時に提供されたシークレットリカバリーフレーズ (シードフレーズ) を入力してください。$1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "アカウント$1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "次の理由により、コレクティブルは追加されませんでした: $1"
- },
"newCollectibleAddedMessage": {
"message": "コレクティブルが追加されました!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "復元"
},
- "restoreAccountWithSeed": {
- "message": "シークレットリカバリーフレーズでアカウントを復元"
- },
"restoreWalletPreferences": {
"message": "$1のデータのバックアップが見つかりました。ウォレットの基本設定を復元しますか?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "このウォレットの最初のアカウントのみが自動的に読み込まれます。 このプロセスの完了後、他のアカウントを追加するには、ドロップダウンメニューをクリックし、[アカウントを作成] を選択します。"
},
- "secretPhraseWarning": {
- "message": "別のシークレットリカバリーフレーズを使用して復元すると、現在のウォレット、アカウント、アセットは永久にこのアプリから削除されます。この操作は元に戻せません。"
- },
"secretRecoveryPhrase": {
"message": "シークレットリカバリーフレーズ"
},
diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json
index 5e74dd375..e6c557c5c 100644
--- a/app/_locales/kn/messages.json
+++ b/app/_locales/kn/messages.json
@@ -765,9 +765,6 @@
"restore": {
"message": "ಮರುಸ್ಥಾಪನೆ"
},
- "restoreAccountWithSeed": {
- "message": "ಸೀಡ್ ಫ್ರೇಸ್ನೊಂದಿಗೆ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಮರುಸ್ಥಾಪಿಸಿ"
- },
"revealSeedWords": {
"message": "ಸೀಡ್ ವರ್ಡ್ಸ್ ಬಹಿರಂಗಪಡಿಸಿ"
},
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index 413bb0212..9814be129 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "계정 가져오기 오류"
},
- "importAccountLinkText": {
- "message": "비밀 복구 구문을 사용해 가져오기"
- },
"importAccountMsg": {
"message": "가져온 계정은 본래 생성한 MetaMask 계정 비밀 복구 구문과 연결하지 못합니다. 가져온 계정에 대해 자세히 알아보기"
},
"importAccountSeedPhrase": {
"message": "비밀 복구 구문으로 계정 가져오기"
},
- "importAccountText": {
- "message": "또는 $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "지갑을 만들 때 받은 비밀 복구 구문(시드 구문)을 입력하세요. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "계정 $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "다음 이유 때문에 수집 금액이 추가되지 않았습니다: $1"
- },
"newCollectibleAddedMessage": {
"message": "수집이 성공적으로 추가되었습니다!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "복구"
},
- "restoreAccountWithSeed": {
- "message": "비밀 복구 구문으로 계정 복구"
- },
"restoreWalletPreferences": {
"message": "$1의 데이터 백업이 발견되었습니다. 지갑 환경설정을 복원할까요?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "금고를 복구하려면 비밀 구문을 여기에 입력하세요."
},
- "secretPhraseWarning": {
- "message": "다른 비밀 복구 구문을 사용하여 복구하면 현재 지갑, 계정 및 자산이 이 앱에서 영구적으로 제거됩니다. 이 작업은 취소할 수 없습니다."
- },
"secretRecoveryPhrase": {
"message": "비밀 복구 구문"
},
diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json
index efb5c8ab8..1a7dd9a51 100644
--- a/app/_locales/lt/messages.json
+++ b/app/_locales/lt/messages.json
@@ -765,9 +765,6 @@
"restore": {
"message": "Atkurti"
},
- "restoreAccountWithSeed": {
- "message": "Atkurti paskyrą naudojant atkūrimo frazę"
- },
"revealSeedWords": {
"message": "Atskleisti atkūrimo žodžius"
},
diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json
index 273904682..adf1c19d4 100644
--- a/app/_locales/lv/messages.json
+++ b/app/_locales/lv/messages.json
@@ -761,9 +761,6 @@
"restore": {
"message": "Atjaunot"
},
- "restoreAccountWithSeed": {
- "message": "Atjaunojiet savu kontu ar atkopšanas frāzi"
- },
"revealSeedWords": {
"message": "Parādīt atkopšanas vārdus"
},
diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json
index 60ca3829c..0675bc2a5 100644
--- a/app/_locales/ms/messages.json
+++ b/app/_locales/ms/messages.json
@@ -745,9 +745,6 @@
"restore": {
"message": "Pulihkan"
},
- "restoreAccountWithSeed": {
- "message": "Pulihkan Akaun anda dengan Ungkapan Benih"
- },
"revealSeedWords": {
"message": "Dedahkan Ungkapan Benih"
},
diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json
index 444a81229..a8bb4af03 100644
--- a/app/_locales/no/messages.json
+++ b/app/_locales/no/messages.json
@@ -752,9 +752,6 @@
"restore": {
"message": "Gjenopprett"
},
- "restoreAccountWithSeed": {
- "message": "Gjenopprett konto med frøfrase"
- },
"revealSeedWords": {
"message": "Vis frøord"
},
diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json
index 6d82062e4..c738b5788 100644
--- a/app/_locales/ph/messages.json
+++ b/app/_locales/ph/messages.json
@@ -842,19 +842,12 @@
"importAccount": {
"message": "Mag-import ng Account"
},
- "importAccountLinkText": {
- "message": "i-import gamit ang Secret Recovery Phrase"
- },
"importAccountMsg": {
"message": " Ang mga na-import na account ay hindi mauugnay sa orihinal mong nagawang Secret Recovery Phrase ng MetaMask account. Matuto pa tungkol sa mga na-import account "
},
"importAccountSeedPhrase": {
"message": "Mag-import ng account gamit ang Secret Recovery Phrase"
},
- "importAccountText": {
- "message": "o $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importTokenQuestion": {
"message": "Mag-import ng token?"
},
@@ -1446,9 +1439,6 @@
"restore": {
"message": "I-restore"
},
- "restoreAccountWithSeed": {
- "message": "I-restore ang iyong Account gamit ang Secret Recovery Phrase"
- },
"restoreWalletPreferences": {
"message": "Nakita ang backup ng iyong data mula sa $1. Gusto mo bang i-restore ang mga kagustuhan mo sa wallet?",
"description": "$1 is the date at which the data was backed up"
diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json
index e37cdeca0..43f9ca385 100644
--- a/app/_locales/pl/messages.json
+++ b/app/_locales/pl/messages.json
@@ -759,9 +759,6 @@
"restore": {
"message": "Przywróć"
},
- "restoreAccountWithSeed": {
- "message": "Przywróć konto frazą seed"
- },
"revealSeedWords": {
"message": "Pokaż słowa seed"
},
diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json
index d6c8a3923..5d1289040 100644
--- a/app/_locales/pt_BR/messages.json
+++ b/app/_locales/pt_BR/messages.json
@@ -1306,19 +1306,12 @@
"importAccountError": {
"message": "Erro de importação de conta."
},
- "importAccountLinkText": {
- "message": "importe usando a Frase de Recuperação Secreta"
- },
"importAccountMsg": {
"message": "As contas importadas não estarão associadas à Frase de Recuperação Secreta da conta da MetaMask criada originalmente. Saiba mais sobre as contas importadas"
},
"importAccountSeedPhrase": {
"message": "Importe uma carteira com a Frase de Recuperação Secreta"
},
- "importAccountText": {
- "message": "ou $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Digite sua Frase de Recuperação Secreta (ou seja, a frase seed) que lhe foi dada quando você criou a sua carteira. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1778,9 +1771,6 @@
"message": "Conta $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "O colecionável não foi adicionado pelo seguinte motivo: $1"
- },
"newCollectibleAddedMessage": {
"message": "O colecionável foi adicionado com sucesso!"
},
@@ -2262,9 +2252,6 @@
"restore": {
"message": "Restaurar"
},
- "restoreAccountWithSeed": {
- "message": "Restaure sua conta com a Frase de Recuperação Secreta"
- },
"restoreWalletPreferences": {
"message": "Encontramos um backup dos seus dados de $1. Gostaria de restaurar as preferências da sua carteira?",
"description": "$1 is the date at which the data was backed up"
@@ -2332,9 +2319,6 @@
"secretPhrase": {
"message": "Somente a primeira conta nessa carteira será carregada automaticamente. Após concluir esse processo, para adicionar mais contas, clique no menu suspenso e selecione Criar Conta."
},
- "secretPhraseWarning": {
- "message": "Se você restaurar usando outra Frase de Recuperação Secreta, sua carteira, conta e ativos atuais serão removidos permanentemente deste aplicativo. Essa ação será irreversível."
- },
"secretRecoveryPhrase": {
"message": "Frase de Recuperação Secreta"
},
diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json
index 37135974e..387fac7bb 100644
--- a/app/_locales/ro/messages.json
+++ b/app/_locales/ro/messages.json
@@ -752,9 +752,6 @@
"restore": {
"message": "Restabilește"
},
- "restoreAccountWithSeed": {
- "message": "Restaurați-vă contul folosind fraza inițială"
- },
"revealSeedWords": {
"message": "Arată cuvintele din seed"
},
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index 23c46ab20..b2dd593ba 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Ошибка импорта счета."
},
- "importAccountLinkText": {
- "message": "импортировать с использованием секретной фразы для восстановления"
- },
"importAccountMsg": {
"message": "Импортированные счета не будут связаны с секретной фразой для восстановления вашего изначально созданного счета MetaMask. Узнайте больше об импортированных счетах"
},
"importAccountSeedPhrase": {
"message": "Импорт кошелька с помощью секретной фразы для восстановления"
},
- "importAccountText": {
- "message": "или $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Введите секретную фразу для восстановления (также известную как «сид-фраза»), которую вы получили при создании кошелька. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Счет $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Причина, по которой не был добавлен коллекционный актив: $1"
- },
"newCollectibleAddedMessage": {
"message": "Коллекционный актив успешно добавлен!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Восстановить"
},
- "restoreAccountWithSeed": {
- "message": "Восстановите свой счет с помощью секретной фразы для восстановления"
- },
"restoreWalletPreferences": {
"message": "Найдена резервная копия ваших данных из $1. Хотите восстановить настройки кошелька?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Автоматически загружается только первый счет в этом кошельке. Для добавления дополнительных счетов, после завершения этого процесса нажмите на выпадающее меню, а затем выберите «Создать счет»."
},
- "secretPhraseWarning": {
- "message": "Если вы выполняете восстановление с использованием другой секретной фразы для восстановления, ваш текущий кошелек, счета и активы будут удалены из этого приложения без возможности восстановления. Это действие нельзя отменить."
- },
"secretRecoveryPhrase": {
"message": "Секретная фраза для восстановления"
},
diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json
index 5e818d979..396bdf170 100644
--- a/app/_locales/sk/messages.json
+++ b/app/_locales/sk/messages.json
@@ -734,9 +734,6 @@
"restore": {
"message": "Obnoviť"
},
- "restoreAccountWithSeed": {
- "message": "Obnoviť účet pomocou seed frázy"
- },
"revealSeedWords": {
"message": "Zobrazit slova klíčové fráze"
},
diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json
index d2cfea50b..490e9ed0c 100644
--- a/app/_locales/sl/messages.json
+++ b/app/_locales/sl/messages.json
@@ -753,9 +753,6 @@
"restore": {
"message": "Obnovi"
},
- "restoreAccountWithSeed": {
- "message": "Obnovi račun z seed phrase"
- },
"revealSeedWords": {
"message": "Razkrij seed words"
},
diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json
index c6c35376c..63a52f898 100644
--- a/app/_locales/sr/messages.json
+++ b/app/_locales/sr/messages.json
@@ -756,9 +756,6 @@
"restore": {
"message": "Поново отвори"
},
- "restoreAccountWithSeed": {
- "message": "Povratite svoj nalog uz pomoć seed fraze"
- },
"revealSeedWords": {
"message": "Otkrivanje početnih reči"
},
diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json
index 0000c4949..c3fa28d56 100644
--- a/app/_locales/sv/messages.json
+++ b/app/_locales/sv/messages.json
@@ -749,9 +749,6 @@
"restore": {
"message": "Återställ"
},
- "restoreAccountWithSeed": {
- "message": "Återställ ditt konto med seedphrase"
- },
"revealSeedWords": {
"message": "Visa seed-ord"
},
diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json
index 247318676..8c4eb4f27 100644
--- a/app/_locales/sw/messages.json
+++ b/app/_locales/sw/messages.json
@@ -743,9 +743,6 @@
"restore": {
"message": "Rejesha"
},
- "restoreAccountWithSeed": {
- "message": "Rejesha Akaunti yako kwa kutumia Kirai Kianzio."
- },
"revealSeedWords": {
"message": "Onyesha Maneno ya Kianzio"
},
diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json
index 20d2c1182..64bd5b6fc 100644
--- a/app/_locales/tl/messages.json
+++ b/app/_locales/tl/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Error sa pag-import ng account."
},
- "importAccountLinkText": {
- "message": "i-import gamit ang Secret Recovery Phrase"
- },
"importAccountMsg": {
"message": "Ang mga na-import na account ay hindi mauugnay sa orihinal mong nagawang Secret Recovery Phrase ng MetaMask account. Matuto pa tungkol sa mga na-import account"
},
"importAccountSeedPhrase": {
"message": "Mag-import ng account gamit ang Secret Recovery Phrase"
},
- "importAccountText": {
- "message": "o $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Ilagay ang iyong Secret Recovery Phrase (kilala rin bilang Seed Phrase) na ibinigay sa iyo noong gumawa ka ng iyong wallet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Account $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Ang collectible ay hindi idinagdag dahil: $1"
- },
"newCollectibleAddedMessage": {
"message": "Ang collectible ay tagumpay na naidagdag!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "I-restore"
},
- "restoreAccountWithSeed": {
- "message": "I-restore ang iyong Account gamit ang Secret Recovery Phrase"
- },
"restoreWalletPreferences": {
"message": "Nakita ang backup ng iyong data mula sa $1. Gusto mo bang i-restore ang mga kagustuhan mo sa wallet?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Ang unang account lang sa wallet na ito ang awtomatikong maglo-load. Pagkatapos makumpleto ang prosesong ito, upang magdagdag ng mga karagdagang account, i-click ang drop down na menu, pagkatapos ay piliin ang Gumawa ng Account."
},
- "secretPhraseWarning": {
- "message": "Kapag nagre-restore ka gamit ang isa pang Secret Recovery Phrase, permanenteng aalisin sa app na ito ang iyong kasalukuyang wallet, mga account, at asset. Ang gawaing ito ay hindi pwedeng baguhin."
- },
"secretRecoveryPhrase": {
"message": "Secret Recovery Phrase"
},
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index a6d03b419..1f19939af 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Hesap içe aktarılırken hata oluştu."
},
- "importAccountLinkText": {
- "message": "Gizli Kurtarma İfadesi kullanarak içe aktar"
- },
"importAccountMsg": {
"message": "İçe aktarılan hesaplar ilk olarak oluşturduğunuz MetaMask hesabı Gizli Kurtarma ifadenizle ilişkilendirilmez. İçe aktarılan hesaplar hakkında daha fazla bilgi edinin"
},
"importAccountSeedPhrase": {
"message": "Gizli Kurtarma İfadesi ile bir cüzdanı içe aktarın"
},
- "importAccountText": {
- "message": "veya $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Cüzdanınızı oluşturduğunuzda size verilen Gizli Kurtarma İfadenizi (başka bir deyişle Tohum İfadesi) girin. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Hesap $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Tahsil edilebilir tutar eklenmedi ve sebebi: $1"
- },
"newCollectibleAddedMessage": {
"message": "Tahsil edilebilir tutar başarılı bir şekilde eklendi!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Geri Yükle"
},
- "restoreAccountWithSeed": {
- "message": "Gizli Kurtarma İfadesi ile Hesabınızı geri yükleyin"
- },
"restoreWalletPreferences": {
"message": "Verilerinizin $1 tarihinden bir yedeği bulundu. Cüzdan tercihlerinizi geri yüklemek ister misiniz?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Sadece bu cüzdandaki ilk hesap otomatik olarak yüklenecektir. Bu işlem tamamlandıktan sonra ilave hesaplar eklemek için açılır menüye tıklayın ardından Hesap Oluştur seçeneğini seçin."
},
- "secretPhraseWarning": {
- "message": "Başka bir Gizli Kurtarma İfadesini kullanarak geri yükleme işlemi yaparsanız mevcut cüzdan, hesap ve varlıklarınız bu uygulamadan kalıcı olarak silinir. Bu işlem geri alınamaz."
- },
"secretRecoveryPhrase": {
"message": "Gizli Kurtarma İfadesi"
},
diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json
index ec6b77371..ed521399c 100644
--- a/app/_locales/uk/messages.json
+++ b/app/_locales/uk/messages.json
@@ -765,9 +765,6 @@
"restore": {
"message": "Відновити"
},
- "restoreAccountWithSeed": {
- "message": "Відновіть ваш обліковий запис за допомогою seed-фрази"
- },
"revealSeedWords": {
"message": "Показати мнемонічні слова"
},
diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json
index 503ce3495..d3cc6931c 100644
--- a/app/_locales/vi/messages.json
+++ b/app/_locales/vi/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "Lỗi khi nhập tài khoản."
},
- "importAccountLinkText": {
- "message": "nhập bằng Cụm mật khẩu khôi phục bí mật"
- },
"importAccountMsg": {
"message": "Tài khoản đã nhập sẽ không được liên kết với Cụm mật khẩu khôi phục bí mật cho tài khoản MetaMask đã tạo ban đầu của bạn. Tìm hiểu thêm về các tài khoản đã nhập"
},
"importAccountSeedPhrase": {
"message": "Nhập một ví bằng Cụm mật khẩu khôi phục bí mật"
},
- "importAccountText": {
- "message": "hoặc $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "Nhập Cụm Mật Khẩu Khôi Phục Bí Mật (còn được gọi là Cụm Mật Khẩu Gốc) mà bạn được cấp khi tạo ví. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "Tài khoản $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "Bộ sưu tập đã không được thêm vì: $1"
- },
"newCollectibleAddedMessage": {
"message": "Bộ sưu tập đã được thêm thành công!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "Khôi phục"
},
- "restoreAccountWithSeed": {
- "message": "Khôi phục tài khoản của bạn bằng cụm mật khẩu khôi phục bí mật"
- },
"restoreWalletPreferences": {
"message": "Đã tìm thấy bản sao lưu dữ liệu của bạn từ $1. Bạn có muốn khôi phục các tùy chọn ưu tiên trong ví của mình không?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "Chỉ tự động tải tài khoản đầu tên trên ví. Sau khi hoàn tất quá trình này, để thêm tài khoản bổ sung, hãy nhấn vào trình đơn thả xuống và chọn Tạo tài khoản."
},
- "secretPhraseWarning": {
- "message": "Nếu bạn khôi phục bằng cách sử dụng một Cụm Mật Khẩu Khôi Phục Bí Mật khác, thì ví, tài khoản và tài sản hiện tại của bạn sẽ bị xóa khỏi ứng dụng này vĩnh viễn. Không thể hoàn tác hành động này."
- },
"secretRecoveryPhrase": {
"message": "Cụm Mật Khẩu Khôi Phục Bí Mật"
},
diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json
index 1e662c7b1..d9c4aa6ea 100644
--- a/app/_locales/zh_CN/messages.json
+++ b/app/_locales/zh_CN/messages.json
@@ -1273,19 +1273,12 @@
"importAccountError": {
"message": "导入帐户时出错。"
},
- "importAccountLinkText": {
- "message": "使用账户助记词导入"
- },
"importAccountMsg": {
"message": "导入的账户将不会与最初创建的 MetaMask 账户助记词相关联。了解更多有关导入账户的信息 。"
},
"importAccountSeedPhrase": {
"message": "使用账户助记词导入账户"
},
- "importAccountText": {
- "message": "或 $1",
- "description": "$1 represents the text from `importAccountLinkText` as a link"
- },
"importExistingWalletDescription": {
"message": "输入您创建$1钱包时提供的保密恢复短语(或Seed Phrase)。",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
@@ -1745,9 +1738,6 @@
"message": "账户 $1",
"description": "Default name of next account to be created on create account screen"
},
- "newCollectibleAddFailed": {
- "message": "未添加收藏,因为:$1"
- },
"newCollectibleAddedMessage": {
"message": "收藏已成功添加!"
},
@@ -2229,9 +2219,6 @@
"restore": {
"message": "恢复"
},
- "restoreAccountWithSeed": {
- "message": "使用账户助记词恢复您的账户"
- },
"restoreWalletPreferences": {
"message": "已找到于 $1 的数据备份。您想恢复您的钱包设置吗?",
"description": "$1 is the date at which the data was backed up"
@@ -2299,9 +2286,6 @@
"secretPhrase": {
"message": "只有这个钱包上的第一个帐户将自动加载。 完成此流程后,点击下拉菜单,然后选择创建账户。"
},
- "secretPhraseWarning": {
- "message": "如果您使用另一个账户助记词来还原,您当前的钱包、帐户和资产将永久从这个应用中移除。 此操作不能撤消。"
- },
"secretRecoveryPhrase": {
"message": "账户助记词"
},
diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json
index 92d7e10b9..ba978532e 100644
--- a/app/_locales/zh_TW/messages.json
+++ b/app/_locales/zh_TW/messages.json
@@ -747,9 +747,6 @@
"resetAccountDescription": {
"message": "重置帳戶將清除您的交易紀錄"
},
- "restoreAccountWithSeed": {
- "message": "透過助憶詞還原您的帳戶"
- },
"revealSeedWords": {
"message": "顯示助憶詞"
},
diff --git a/app/images/arbitrum.svg b/app/images/arbitrum.svg
new file mode 100644
index 000000000..5e79c8ccd
--- /dev/null
+++ b/app/images/arbitrum.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/images/logo/metamask-smart-transactions@4x.png b/app/images/logo/metamask-smart-transactions@4x.png
deleted file mode 100644
index 636576495..000000000
Binary files a/app/images/logo/metamask-smart-transactions@4x.png and /dev/null differ
diff --git a/app/images/logo/smart-transactions-header.png b/app/images/logo/smart-transactions-header.png
new file mode 100644
index 000000000..d0fa8ea94
Binary files /dev/null and b/app/images/logo/smart-transactions-header.png differ
diff --git a/app/images/optimism.svg b/app/images/optimism.svg
new file mode 100644
index 000000000..e8c6e64e4
--- /dev/null
+++ b/app/images/optimism.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/images/times.svg b/app/images/times.svg
new file mode 100644
index 000000000..5aa9be3a5
--- /dev/null
+++ b/app/images/times.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/scripts/controllers/onboarding.js b/app/scripts/controllers/onboarding.js
index 370ca5e8e..812ba7e44 100644
--- a/app/scripts/controllers/onboarding.js
+++ b/app/scripts/controllers/onboarding.js
@@ -69,7 +69,7 @@ export default class OnboardingController {
* @param {string} tabId - The id of the tab registering
*/
registerOnboarding = async (location, tabId) => {
- if (this.completedOnboarding) {
+ if (this.store.getState().completedOnboarding) {
log.debug('Ignoring registerOnboarding; user already onboarded');
return;
}
diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js
index 04eae26f8..93947c1e2 100644
--- a/app/scripts/controllers/transactions/index.js
+++ b/app/scripts/controllers/transactions/index.js
@@ -10,6 +10,7 @@ import { ethers } from 'ethers';
import NonceTracker from 'nonce-tracker';
import log from 'loglevel';
import BigNumber from 'bignumber.js';
+import { merge, pickBy } from 'lodash';
import cleanErrorStack from '../../lib/cleanErrorStack';
import {
hexToBn,
@@ -130,6 +131,8 @@ export default class TransactionController extends EventEmitter {
this.updateEventFragment = opts.updateEventFragment;
this.finalizeEventFragment = opts.finalizeEventFragment;
this.getEventFragmentById = opts.getEventFragmentById;
+ this.getDeviceModel = opts.getDeviceModel;
+ this.getAccountType = opts.getAccountType;
this.memStore = new ObservableStore({});
this.query = new EthQuery(this.provider);
@@ -347,6 +350,268 @@ export default class TransactionController extends EventEmitter {
});
}
+ // ====================================================================================================================================================
+
+ /**
+ * @param {number} txId
+ * @returns {TransactionMeta} the txMeta who matches the given id if none found
+ * for the network returns undefined
+ */
+ _getTransaction(txId) {
+ const { transactions } = this.store.getState();
+ return transactions[txId];
+ }
+
+ _checkIfTxStatusIsUnapproved(txId) {
+ return (
+ this.txStateManager.getTransaction(txId).status ===
+ TRANSACTION_STATUSES.UNAPPROVED
+ );
+ }
+
+ _updateTransaction(txId, proposedUpdate, note) {
+ const txMeta = this.txStateManager.getTransaction(txId);
+ const updated = merge(txMeta, proposedUpdate);
+ this.txStateManager.updateTransaction(updated, note);
+ }
+
+ /**
+ *
+ * @param {string} txId - transaction id
+ * @param {object} editableParams - holds the eip1559 fees parameters
+ * @param editableParams.data
+ * @param editableParams.from
+ * @param editableParams.to
+ * @param editableParams.value
+ * @param editableParams.gas
+ * @param editableParams.gasPrice
+ */
+ updateEditableParams(txId, { data, from, to, value, gas, gasPrice }) {
+ if (!this._checkIfTxStatusIsUnapproved(txId)) {
+ return;
+ }
+
+ const editableParams = {
+ txParams: {
+ data,
+ from,
+ to,
+ value,
+ gas,
+ gasPrice,
+ },
+ };
+
+ // only update what is defined
+ editableParams.txParams = pickBy(editableParams.txParams);
+ const note = `Update Editable Params for ${txId}`;
+ this._updateTransaction(txId, editableParams, note);
+ }
+
+ /**
+ * updates the gas fees of the transaction with id if the transaction state is unapproved
+ *
+ * @param {string} txId - transaction id
+ * @param {object} txGasFees - holds the gas fees parameters
+ * {
+ * gasLimit,
+ * gasPrice,
+ * maxPriorityFeePerGas,
+ * maxFeePerGas,
+ * estimateUsed,
+ * estimateSuggested
+ * }
+ * @param txGasFees.gasLimit
+ * @param txGasFees.gasPrice
+ * @param txGasFees.maxPriorityFeePerGas
+ * @param txGasFees.maxFeePerGas
+ * @param txGasFees.estimateUsed
+ * @param txGasFees.estimateSuggested
+ * @param txGasFees.defaultGasEstimates
+ * @param txGasFees.gas
+ * @param txGasFees.originalGasEstimate
+ */
+ updateTransactionGasFees(
+ txId,
+ {
+ gas,
+ gasLimit,
+ gasPrice,
+ maxPriorityFeePerGas,
+ maxFeePerGas,
+ estimateUsed,
+ estimateSuggested,
+ defaultGasEstimates,
+ originalGasEstimate,
+ },
+ ) {
+ if (!this._checkIfTxStatusIsUnapproved(txId)) {
+ return;
+ }
+
+ let txGasFees = {
+ txParams: {
+ gas,
+ gasLimit,
+ gasPrice,
+ maxPriorityFeePerGas,
+ maxFeePerGas,
+ },
+ estimateUsed,
+ estimateSuggested,
+ defaultGasEstimates,
+ originalGasEstimate,
+ };
+
+ // only update what is defined
+ txGasFees.txParams = pickBy(txGasFees.txParams);
+ txGasFees = pickBy(txGasFees);
+ const note = `Update Transaction Gas Fees for ${txId}`;
+ this._updateTransaction(txId, txGasFees, note);
+ }
+
+ /**
+ * updates the estimate base fees of the transaction with id if the transaction state is unapproved
+ *
+ * @param {string} txId - transaction id
+ * @param {object} txEstimateBaseFees - holds the estimate base fees parameters
+ * {
+ * estimatedBaseFee,
+ * decEstimatedBaseFee
+ * }
+ * @param txEstimateBaseFees.estimatedBaseFee
+ * @param txEstimateBaseFees.decEstimatedBaseFee
+ */
+ updateTransactionEstimatedBaseFee(
+ txId,
+ { estimatedBaseFee, decEstimatedBaseFee },
+ ) {
+ if (!this._checkIfTxStatusIsUnapproved(txId)) {
+ return;
+ }
+
+ let txEstimateBaseFees = { estimatedBaseFee, decEstimatedBaseFee };
+ // only update what is defined
+ txEstimateBaseFees = pickBy(txEstimateBaseFees);
+
+ const note = `Update Transaction Estimated Base Fees for ${txId}`;
+ this._updateTransaction(txId, txEstimateBaseFees, note);
+ }
+
+ /**
+ * updates a swap approval transaction with provided metadata and source token symbol
+ * if the transaction state is unapproved.
+ *
+ * @param {string} txId
+ * @param {object} swapApprovalTransaction - holds the metadata and token symbol
+ * {
+ * type,
+ * sourceTokenSymbol
+ * }
+ * @param swapApprovalTransaction.type
+ * @param swapApprovalTransaction.sourceTokenSymbol
+ */
+ updateSwapApprovalTransaction(txId, { type, sourceTokenSymbol }) {
+ if (!this._checkIfTxStatusIsUnapproved(txId)) {
+ return;
+ }
+
+ let swapApprovalTransaction = { type, sourceTokenSymbol };
+ // only update what is defined
+ swapApprovalTransaction = pickBy(swapApprovalTransaction);
+
+ const note = `Update Swap Approval Transaction for ${txId}`;
+ this._updateTransaction(txId, swapApprovalTransaction, note);
+ }
+
+ /**
+ * updates a swap transaction with provided metadata and source token symbol
+ * if the transaction state is unapproved.
+ *
+ * @param {string} txId
+ * @param {object} swapTransaction - holds the metadata
+ * {
+ * sourceTokenSymbol,
+ * destinationTokenSymbol,
+ * type,
+ * destinationTokenDecimals,
+ * destinationTokenAddress,
+ * swapMetaData,
+ * swapTokenValue,
+ * estimatedBaseFee,
+ * approvalTxId
+ *}
+ * @param swapTransaction.sourceTokenSymbol
+ * @param swapTransaction.destinationTokenSymbol
+ * @param swapTransaction.type
+ * @param swapTransaction.destinationTokenDecimals
+ * @param swapTransaction.destinationTokenAddress
+ * @param swapTransaction.swapMetaData
+ * @param swapTransaction.swapTokenValue
+ * @param swapTransaction.estimatedBaseFee
+ * @param swapTransaction.approvalTxId
+ */
+ updateSwapTransaction(
+ txId,
+ {
+ sourceTokenSymbol,
+ destinationTokenSymbol,
+ type,
+ destinationTokenDecimals,
+ destinationTokenAddress,
+ swapMetaData,
+ swapTokenValue,
+ estimatedBaseFee,
+ approvalTxId,
+ },
+ ) {
+ if (!this._checkIfTxStatusIsUnapproved(txId)) {
+ return;
+ }
+
+ let swapTransaction = {
+ sourceTokenSymbol,
+ destinationTokenSymbol,
+ type,
+ destinationTokenDecimals,
+ destinationTokenAddress,
+ swapMetaData,
+ swapTokenValue,
+ estimatedBaseFee,
+ approvalTxId,
+ };
+
+ // only update what is defined
+ swapTransaction = pickBy(swapTransaction);
+
+ const note = `Update Swap Transaction for ${txId}`;
+ this._updateTransaction(txId, swapTransaction, note);
+ }
+
+ /**
+ * updates a transaction's user settings only if the transaction state is unapproved
+ *
+ * @param {string} txId
+ * @param {object} userSettings - holds the metadata
+ * { userEditedGasLimit, userFeeLevel }
+ * @param userSettings.userEditedGasLimit
+ * @param userSettings.userFeeLevel
+ */
+ updateTransactionUserSettings(txId, { userEditedGasLimit, userFeeLevel }) {
+ if (!this._checkIfTxStatusIsUnapproved(txId)) {
+ return;
+ }
+
+ let userSettings = { userEditedGasLimit, userFeeLevel };
+ // only update what is defined
+ userSettings = pickBy(userSettings);
+
+ const note = `Update User Settings for ${txId}`;
+ this._updateTransaction(txId, userSettings, note);
+ }
+
+ // ====================================================================================================================================================
+
/**
* Validates and generates a txMeta with defaults and puts it in txStateManager
* store.
@@ -1735,6 +2000,8 @@ export default class TransactionController extends EventEmitter {
eip_1559_version: eip1559Version,
gas_edit_type: 'none',
gas_edit_attempted: 'none',
+ account_type: await this.getAccountType(this.getSelectedAddress()),
+ device_model: await this.getDeviceModel(this.getSelectedAddress()),
};
const sensitiveProperties = {
diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js
index c20fd5459..37501acfb 100644
--- a/app/scripts/controllers/transactions/index.test.js
+++ b/app/scripts/controllers/transactions/index.test.js
@@ -82,6 +82,8 @@ describe('Transaction Controller', function () {
getEventFragmentById: () =>
fragmentExists === false ? undefined : { id: 0 },
getEIP1559GasFeeEstimates: () => undefined,
+ getAccountType: () => 'MetaMask',
+ getDeviceModel: () => 'N/A',
});
txController.nonceTracker.getNonceLock = () =>
Promise.resolve({ nextNonce: 0, releaseLock: noop });
@@ -1616,6 +1618,8 @@ describe('Transaction Controller', function () {
referrer: 'metamask',
source: 'user',
type: TRANSACTION_TYPES.SIMPLE_SEND,
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
default_gas: '0.000031501',
@@ -1691,6 +1695,8 @@ describe('Transaction Controller', function () {
referrer: 'metamask',
source: 'user',
type: TRANSACTION_TYPES.SIMPLE_SEND,
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
default_gas: '0.000031501',
@@ -1776,6 +1782,8 @@ describe('Transaction Controller', function () {
referrer: 'other',
source: 'dapp',
type: TRANSACTION_TYPES.SIMPLE_SEND,
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
default_gas: '0.000031501',
@@ -1853,6 +1861,8 @@ describe('Transaction Controller', function () {
referrer: 'other',
source: 'dapp',
type: TRANSACTION_TYPES.SIMPLE_SEND,
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
default_gas: '0.000031501',
@@ -1930,6 +1940,8 @@ describe('Transaction Controller', function () {
referrer: 'other',
source: 'dapp',
type: TRANSACTION_TYPES.SIMPLE_SEND,
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
gas_price: '2',
@@ -1989,6 +2001,8 @@ describe('Transaction Controller', function () {
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
baz: 3.0,
@@ -2058,6 +2072,8 @@ describe('Transaction Controller', function () {
referrer: 'other',
source: 'dapp',
type: TRANSACTION_TYPES.SIMPLE_SEND,
+ account_type: 'MetaMask',
+ device_model: 'N/A',
},
sensitiveProperties: {
baz: 3.0,
@@ -2159,4 +2175,203 @@ describe('Transaction Controller', function () {
assert.deepEqual(result, expectedParams);
});
});
+
+ describe('update transaction methods', function () {
+ let txStateManager;
+
+ beforeEach(function () {
+ txStateManager = txController.txStateManager;
+ txStateManager.addTransaction({
+ id: '1',
+ status: TRANSACTION_STATUSES.UNAPPROVED,
+ metamaskNetworkId: currentNetworkId,
+ txParams: {
+ gasLimit: '0x001',
+ gasPrice: '0x002',
+ // max fees can not be mixed with gasPrice
+ // maxPriorityFeePerGas: '0x003',
+ // maxFeePerGas: '0x004',
+ to: VALID_ADDRESS,
+ from: VALID_ADDRESS,
+ },
+ estimateUsed: '0x005',
+ estimatedBaseFee: '0x006',
+ decEstimatedBaseFee: '6',
+ type: 'swap',
+ sourceTokenSymbol: 'ETH',
+ destinationTokenSymbol: 'UNI',
+ destinationTokenDecimals: 16,
+ destinationTokenAddress: VALID_ADDRESS,
+ swapMetaData: {},
+ swapTokenValue: '0x007',
+ userEditedGasLimit: '0x008',
+ userFeeLevel: 'medium',
+ });
+ });
+
+ it('updates transaction gas fees', function () {
+ // test update gasFees
+ txController.updateTransactionGasFees('1', {
+ gasPrice: '0x0022',
+ gasLimit: '0x0011',
+ });
+ let result = txStateManager.getTransaction('1');
+ assert.equal(result.txParams.gasPrice, '0x0022');
+ // TODO: weird behavior here...only gasPrice gets returned.
+ // assert.equal(result.txParams.gasLimit, '0x0011');
+
+ // test update maxPriorityFeePerGas
+ txStateManager.addTransaction({
+ id: '2',
+ status: TRANSACTION_STATUSES.UNAPPROVED,
+ metamaskNetworkId: currentNetworkId,
+ txParams: {
+ maxPriorityFeePerGas: '0x003',
+ to: VALID_ADDRESS,
+ from: VALID_ADDRESS,
+ },
+ estimateUsed: '0x005',
+ });
+ txController.updateTransactionGasFees('2', {
+ maxPriorityFeePerGas: '0x0033',
+ });
+ result = txStateManager.getTransaction('2');
+ assert.equal(result.txParams.maxPriorityFeePerGas, '0x0033');
+
+ // test update maxFeePerGas
+ txStateManager.addTransaction({
+ id: '3',
+ status: TRANSACTION_STATUSES.UNAPPROVED,
+ metamaskNetworkId: currentNetworkId,
+ txParams: {
+ maxPriorityFeePerGas: '0x003',
+ maxFeePerGas: '0x004',
+ to: VALID_ADDRESS,
+ from: VALID_ADDRESS,
+ },
+ estimateUsed: '0x005',
+ });
+ txController.updateTransactionGasFees('3', { maxFeePerGas: '0x0044' });
+ result = txStateManager.getTransaction('3');
+ assert.equal(result.txParams.maxFeePerGas, '0x0044');
+
+ // test update estimate used
+ txController.updateTransactionGasFees('3', { estimateUsed: '0x0055' });
+ result = txStateManager.getTransaction('3');
+ assert.equal(result.estimateUsed, '0x0055');
+ });
+
+ it('updates estimated base fee', function () {
+ txController.updateTransactionEstimatedBaseFee('1', {
+ estimatedBaseFee: '0x0066',
+ decEstimatedBaseFee: '66',
+ });
+ const result = txStateManager.getTransaction('1');
+ assert.equal(result.estimatedBaseFee, '0x0066');
+ assert.equal(result.decEstimatedBaseFee, '66');
+ });
+
+ it('updates swap approval transaction', function () {
+ txController.updateSwapApprovalTransaction('1', {
+ type: 'swapApproval',
+ sourceTokenSymbol: 'XBN',
+ });
+
+ const result = txStateManager.getTransaction('1');
+ assert.equal(result.type, 'swapApproval');
+ assert.equal(result.sourceTokenSymbol, 'XBN');
+ });
+
+ it('updates swap transaction', function () {
+ txController.updateSwapTransaction('1', {
+ sourceTokenSymbol: 'BTCX',
+ destinationTokenSymbol: 'ETH',
+ });
+
+ const result = txStateManager.getTransaction('1');
+ assert.equal(result.sourceTokenSymbol, 'BTCX');
+ assert.equal(result.destinationTokenSymbol, 'ETH');
+ assert.equal(result.destinationTokenDecimals, 16);
+ assert.equal(result.destinationTokenAddress, VALID_ADDRESS);
+ assert.equal(result.swapTokenValue, '0x007');
+
+ txController.updateSwapTransaction('1', {
+ type: 'swapped',
+ destinationTokenDecimals: 8,
+ destinationTokenAddress: VALID_ADDRESS_TWO,
+ swapTokenValue: '0x0077',
+ });
+ assert.equal(result.sourceTokenSymbol, 'BTCX');
+ assert.equal(result.destinationTokenSymbol, 'ETH');
+ assert.equal(result.type, 'swapped');
+ assert.equal(result.destinationTokenDecimals, 8);
+ assert.equal(result.destinationTokenAddress, VALID_ADDRESS_TWO);
+ assert.equal(result.swapTokenValue, '0x0077');
+ });
+
+ it('updates transaction user settings', function () {
+ txController.updateTransactionUserSettings('1', {
+ userEditedGasLimit: '0x0088',
+ userFeeLevel: 'high',
+ });
+
+ const result = txStateManager.getTransaction('1');
+ assert.equal(result.userEditedGasLimit, '0x0088');
+ assert.equal(result.userFeeLevel, 'high');
+ });
+
+ it('does not update if status is not unapproved', function () {
+ txStateManager.addTransaction({
+ id: '4',
+ status: TRANSACTION_STATUSES.APPROVED,
+ metamaskNetworkId: currentNetworkId,
+ txParams: {
+ maxPriorityFeePerGas: '0x007',
+ maxFeePerGas: '0x008',
+ to: VALID_ADDRESS,
+ from: VALID_ADDRESS,
+ },
+ estimateUsed: '0x009',
+ });
+
+ txController.updateTransactionGasFees('4', { maxFeePerGas: '0x0088' });
+ let result = txStateManager.getTransaction('4');
+ assert.equal(result.txParams.maxFeePerGas, '0x008');
+
+ // test update estimate used
+ txController.updateTransactionGasFees('4', { estimateUsed: '0x0099' });
+ result = txStateManager.getTransaction('4');
+ assert.equal(result.estimateUsed, '0x009');
+ });
+
+ it('does not update unknown parameters in update method', function () {
+ txController.updateSwapTransaction('1', {
+ type: 'swapped',
+ destinationTokenDecimals: 8,
+ destinationTokenAddress: VALID_ADDRESS_TWO,
+ swapTokenValue: '0x011',
+ gasPrice: '0x12',
+ });
+
+ let result = txStateManager.getTransaction('1');
+
+ assert.equal(result.type, 'swapped');
+ assert.equal(result.destinationTokenDecimals, 8);
+ assert.equal(result.destinationTokenAddress, VALID_ADDRESS_TWO);
+ assert.equal(result.swapTokenValue, '0x011');
+ assert.equal(result.txParams.gasPrice, '0x002'); // not updated even though it's passed in to update
+
+ txController.updateTransactionGasFees('1', {
+ estimateUsed: '0x13',
+ gasPrice: '0x14',
+ destinationTokenAddress: VALID_ADDRESS,
+ });
+
+ result = txStateManager.getTransaction('1');
+ console.log(result);
+ assert.equal(result.estimateUsed, '0x13');
+ assert.equal(result.txParams.gasPrice, '0x14');
+ assert.equal(result.destinationTokenAddress, VALID_ADDRESS_TWO); // not updated even though it's passed in to update
+ });
+ });
});
diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js
index 879aac56c..d59b5a058 100644
--- a/app/scripts/controllers/transactions/tx-state-manager.js
+++ b/app/scripts/controllers/transactions/tx-state-manager.js
@@ -249,9 +249,9 @@ export default class TransactionStateManager extends EventEmitter {
const txsToDelete = transactions
.reverse()
.filter((tx) => {
- const { nonce } = tx.txParams;
+ const { nonce, from } = tx.txParams;
const { chainId, metamaskNetworkId, status } = tx;
- const key = `${nonce}-${chainId ?? metamaskNetworkId}`;
+ const key = `${nonce}-${chainId ?? metamaskNetworkId}-${from}`;
if (nonceNetworkSet.has(key)) {
return false;
} else if (
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index c378114a1..b7ddb2a5a 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -671,7 +671,7 @@ export default class MetamaskController extends EventEmitter {
this.networkController,
),
preferencesStore: this.preferencesController.store,
- txHistoryLimit: 40,
+ txHistoryLimit: 60,
signTransaction: this.keyringController.signTransaction.bind(
this.keyringController,
),
@@ -700,6 +700,8 @@ export default class MetamaskController extends EventEmitter {
getExternalPendingTransactions: this.getExternalPendingTransactions.bind(
this,
),
+ getAccountType: this.getAccountType.bind(this),
+ getDeviceModel: this.getDeviceModel.bind(this),
});
this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation());
@@ -2186,6 +2188,54 @@ export default class MetamaskController extends EventEmitter {
return true;
}
+ /**
+ * Retrieves the keyring for the selected address and using the .type returns
+ * a subtype for the account. Either 'hardware', 'imported' or 'MetaMask'.
+ *
+ * @param {string} address - Address to retrieve keyring for
+ * @returns {'hardware' | 'imported' | 'MetaMask'}
+ */
+ async getAccountType(address) {
+ const keyring = await this.keyringController.getKeyringForAccount(address);
+ switch (keyring.type) {
+ case KEYRING_TYPES.TREZOR:
+ case KEYRING_TYPES.LATTICE:
+ case KEYRING_TYPES.QR:
+ case KEYRING_TYPES.LEDGER:
+ return 'hardware';
+ case KEYRING_TYPES.IMPORTED:
+ return 'imported';
+ default:
+ return 'MetaMask';
+ }
+ }
+
+ /**
+ * Retrieves the keyring for the selected address and using the .type
+ * determines if a more specific name for the device is available. Returns
+ * 'N/A' for non hardware wallets.
+ *
+ * @param {string} address - Address to retrieve keyring for
+ * @returns {'ledger' | 'lattice' | 'N/A' | string}
+ */
+ async getDeviceModel(address) {
+ const keyring = await this.keyringController.getKeyringForAccount(address);
+ switch (keyring.type) {
+ case KEYRING_TYPES.TREZOR:
+ return keyring.getModel();
+ case KEYRING_TYPES.QR:
+ return keyring.getName();
+ case KEYRING_TYPES.LEDGER:
+ // TODO: get model after ledger keyring exposes method
+ return DEVICE_NAMES.LEDGER;
+ case KEYRING_TYPES.LATTICE:
+ // TODO: get model after lattice keyring exposes method
+ return DEVICE_NAMES.LATTICE;
+ default:
+ return 'N/A';
+ }
+ }
+
/**
* get hardware account label
*
diff --git a/development/build/README.md b/development/build/README.md
index 7d6cd8d9c..85823992b 100644
--- a/development/build/README.md
+++ b/development/build/README.md
@@ -1,6 +1,7 @@
# The MetaMask Build System
-> _tl;dr_ `yarn dist` for prod, `yarn start` for local development
+> _tl;dr_ `yarn dist` for prod, `yarn start` for local development.
+> Add `--build-type flask` to build Flask, our canary distribution with more experimental features.
This directory contains the MetaMask build system, which is used to build the MetaMask Extension such that it can be used in a supported browser.
From the repository root, the build system entry file is located at [`./development/build/index.js`](https://github.com/MetaMask/metamask-extension/blob/develop/development/build/index.js).
@@ -40,7 +41,8 @@ Commands:
e2e tests.
Options:
- --build-type The "type" of build to create. One of: "beta", "main"
+ --build-type The "type" of build to create. One of: "beta", "flask",
+ "main"
[string] [default: "main"]
--lint-fence-files Whether files with code fences should be linted after
fences have been removed by the code fencing transform.
diff --git a/development/build/transforms/utils.js b/development/build/transforms/utils.js
index d7ab9654f..fb7f49c32 100644
--- a/development/build/transforms/utils.js
+++ b/development/build/transforms/utils.js
@@ -1,11 +1,17 @@
const { ESLint } = require('eslint');
const eslintrc = require('../../../.eslintrc.js');
-// We don't want linting to fail for purely stylistic reasons.
-eslintrc.rules['prettier/prettier'] = 'off';
-// Sometimes we use `let` instead of `const` to assign variables depending on
-// the build type.
-eslintrc.rules['prefer-const'] = 'off';
+eslintrc.overrides.forEach((override) => {
+ const rules = override.rules ?? {};
+
+ // We don't want linting to fail for purely stylistic reasons.
+ rules['prettier/prettier'] = 'off';
+ // Sometimes we use `let` instead of `const` to assign variables depending on
+ // the build type.
+ rules['prefer-const'] = 'off';
+
+ override.rules = rules;
+});
// Remove all test-related overrides. We will never lint test files here.
eslintrc.overrides = eslintrc.overrides.filter((override) => {
diff --git a/development/mock-e2e.js b/development/mock-e2e.js
index 1e466d36e..2eb05e75e 100644
--- a/development/mock-e2e.js
+++ b/development/mock-e2e.js
@@ -1,4 +1,4 @@
-function setupMocking(server) {
+function setupMocking(server, testSpecificMock) {
server.forAnyRequest().thenPassThrough();
server
@@ -27,6 +27,8 @@ function setupMocking(server) {
},
};
});
+
+ testSpecificMock(server);
}
module.exports = { setupMocking };
diff --git a/docs/add-to-chrome.md b/docs/add-to-chrome.md
index 82c1afed3..9e4d8c4a4 100644
--- a/docs/add-to-chrome.md
+++ b/docs/add-to-chrome.md
@@ -2,13 +2,16 @@
![Load dev build](./load-dev-build-chrome.gif)
+* Create a local build of MetaMask using your preferred method.
+ * You can find build instructions in the [readme](https://github.com/MetaMask/metamask-extension#readme).
* Open `Settings` > `Extensions`.
+ * Or go straight to [chrome://extensions](chrome://extensions).
* Check "Developer mode".
-* Alternatively, use the URL `chrome://extensions/` in your address bar
* At the top, click `Load Unpacked Extension`.
-* Navigate to your `metamask-plugin/dist/chrome` folder.
+* Navigate to your `metamask-extension/dist/chrome` folder.
* Click `Select`.
* Change to your locale via `chrome://settings/languages`
-* Restart the browser and test the plugin in your locale
+* Restart the browser and test the extension in your locale
-You now have the plugin, and can click 'inspect views: background plugin' to view its dev console.
+Your dev build is now added to Chrome, and you can click `Inspect views
+background.html` in its card on the extension settings page to view its dev console.
diff --git a/docs/add-to-firefox.md b/docs/add-to-firefox.md
index 20810f9a6..424af6317 100644
--- a/docs/add-to-firefox.md
+++ b/docs/add-to-firefox.md
@@ -1,14 +1,11 @@
# Add Custom Build to Firefox
-Go to the url `about:debugging#addons`.
-
-Click the button `Load Temporary Add-On`.
-
-Select the file `dist/firefox/manifest.json`.
-
-You can optionally enable debugging, and click `Debug`, for a console window that logs all of Metamask's processes to a single console.
+* Create a local build of MetaMask using your preferred method.
+ * You can find build instructions in the [readme](https://github.com/MetaMask/metamask-extension#readme).
+* Go to the url `about:debugging#addons`.
+* Click the button `Load Temporary Add-On`.
+* Select the file `metamask-extension/dist/firefox/manifest.json`.
+* You can optionally enable debugging, and click `Debug`, for a console window that logs all of Metamask's processes to a single console.
If you have problems debugging, try connecting to the IRC channel `#webextensions` on `irc.mozilla.org`.
-
For longer questions, use the StackOverflow tag `firefox-addons`.
-
diff --git a/lavamoat/build-system/policy-override.json b/lavamoat/build-system/policy-override.json
index 34a2f3527..282e26ff0 100644
--- a/lavamoat/build-system/policy-override.json
+++ b/lavamoat/build-system/policy-override.json
@@ -14,6 +14,7 @@
},
"@eslint/eslintrc": {
"packages": {
+ "": true,
"@babel/eslint-parser": true,
"@babel/eslint-plugin": true,
"@metamask/eslint-config": true,
diff --git a/package.json b/package.json
index a5edf84d8..520244234 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
"@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.31.0",
"@metamask/controllers": "^25.0.0",
+ "@metamask/design-tokens": "^1.3.0",
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
"@metamask/eth-token-tracker": "^4.0.0",
"@metamask/etherscan-link": "^2.1.0",
@@ -261,6 +262,7 @@
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^10.4.8",
"@testing-library/react-hooks": "^3.2.1",
+ "@testing-library/user-event": "^14.0.0-beta.12",
"@types/react": "^16.9.53",
"addons-linter": "1.14.0",
"babelify": "^10.0.0",
@@ -347,6 +349,7 @@
"source-map": "^0.7.2",
"source-map-explorer": "^2.4.2",
"squirrelly": "^8.0.8",
+ "storybook-dark-mode": "^1.0.9",
"string.prototype.matchall": "^4.0.2",
"style-loader": "^0.21.0",
"stylelint": "^13.6.1",
diff --git a/shared/constants/hardware-wallets.js b/shared/constants/hardware-wallets.js
index 195a1b488..48bc65d3d 100644
--- a/shared/constants/hardware-wallets.js
+++ b/shared/constants/hardware-wallets.js
@@ -8,6 +8,7 @@ export const KEYRING_TYPES = {
TREZOR: 'Trezor Hardware',
LATTICE: 'Lattice Hardware',
QR: 'QR Hardware Wallet Device',
+ IMPORTED: 'Simple Key Pair',
};
export const DEVICE_NAMES = {
diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js
index 148964b03..aebc6b673 100644
--- a/test/e2e/helpers.js
+++ b/test/e2e/helpers.js
@@ -7,6 +7,7 @@ const {
createSegmentServer,
} = require('../../development/lib/create-segment-server');
const { setupMocking } = require('../../development/mock-e2e');
+const enLocaleMessages = require('../../app/_locales/en/messages.json');
const Ganache = require('./ganache');
const FixtureServer = require('./fixture-server');
const { buildWebDriver } = require('./webdriver');
@@ -29,6 +30,9 @@ async function withFixtures(options, testSuite) {
title,
failOnConsoleError = true,
dappPath = undefined,
+ testSpecificMock = function () {
+ // do nothing.
+ },
} = options;
const fixtureServer = new FixtureServer();
const ganacheServer = new Ganache();
@@ -89,8 +93,8 @@ async function withFixtures(options, testSuite) {
}
const https = await mockttp.generateCACertificate();
mockServer = mockttp.getLocal({ https });
+ setupMocking(mockServer, testSpecificMock);
await mockServer.start(8000);
- setupMocking(mockServer);
if (
process.env.SELENIUM_BROWSER === 'chrome' &&
process.env.CI === 'true'
@@ -203,6 +207,72 @@ const connectDappWithExtensionPopup = async (driver) => {
await driver.delay(regularDelayMs);
};
+const completeImportSRPOnboardingFlow = async (
+ driver,
+ seedPhrase,
+ password,
+) => {
+ if (process.env.ONBOARDING_V2 === '1') {
+ // welcome
+ await driver.clickElement('[data-testid="onboarding-import-wallet"]');
+
+ // metrics
+ await driver.clickElement('[data-testid="metametrics-no-thanks"]');
+
+ // import with recovery phrase
+ await driver.fill('[data-testid="import-srp-text"]', seedPhrase);
+ await driver.clickElement('[data-testid="import-srp-confirm"]');
+
+ // create password
+ await driver.fill('[data-testid="create-password-new"]', password);
+ await driver.fill('[data-testid="create-password-confirm"]', password);
+ await driver.clickElement('[data-testid="create-password-terms"]');
+ await driver.clickElement('[data-testid="create-password-import"]');
+
+ // complete
+ await driver.clickElement('[data-testid="onboarding-complete-done"]');
+
+ // pin extension
+ await driver.clickElement('[data-testid="pin-extension-next"]');
+ await driver.clickElement('[data-testid="pin-extension-done"]');
+ } else {
+ // clicks the continue button on the welcome screen
+ await driver.findElement('.welcome-page__header');
+ await driver.clickElement({
+ text: enLocaleMessages.getStarted.message,
+ tag: 'button',
+ });
+
+ // clicks the "Import Wallet" option
+ await driver.clickElement({ text: 'Import wallet', tag: 'button' });
+
+ // clicks the "No thanks" option on the metametrics opt-in screen
+ await driver.clickElement('.btn-secondary');
+
+ // Import Secret Recovery Phrase
+ await driver.fill(
+ 'input[placeholder="Enter your Secret Recovery Phrase"]',
+ seedPhrase,
+ );
+
+ await driver.fill('#password', password);
+ await driver.fill('#confirm-password', password);
+
+ await driver.clickElement(
+ '[data-testid="create-new-vault__terms-checkbox"]',
+ );
+
+ await driver.clickElement({ text: 'Import', tag: 'button' });
+
+ // clicks through the success screen
+ await driver.findElement({ text: 'Congratulations', tag: 'div' });
+ await driver.clickElement({
+ text: enLocaleMessages.endOfFlowMessage10.message,
+ tag: 'button',
+ });
+ }
+};
+
module.exports = {
getWindowHandles,
convertToHexValue,
@@ -211,4 +281,5 @@ module.exports = {
largeDelayMs,
withFixtures,
connectDappWithExtensionPopup,
+ completeImportSRPOnboardingFlow,
};
diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js
index 7dbc96271..3600ec109 100644
--- a/test/e2e/metamask-ui.spec.js
+++ b/test/e2e/metamask-ui.spec.js
@@ -191,12 +191,9 @@ describe('MetaMask', function () {
it('imports Secret Recovery Phrase', async function () {
const restoreSeedLink = await driver.findClickableElement(
- '.unlock-page__link--import',
- );
- assert.equal(
- await restoreSeedLink.getText(),
- 'import using Secret Recovery Phrase',
+ '.unlock-page__link',
);
+ assert.equal(await restoreSeedLink.getText(), 'Forgot password?');
await restoreSeedLink.click();
await driver.delay(regularDelayMs);
diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js
index e358660d0..5c0d3fcb9 100644
--- a/test/e2e/tests/add-account.spec.js
+++ b/test/e2e/tests/add-account.spec.js
@@ -1,16 +1,26 @@
const { strict: assert } = require('assert');
-const { convertToHexValue, withFixtures } = require('../helpers');
+const {
+ convertToHexValue,
+ withFixtures,
+ regularDelayMs,
+ completeImportSRPOnboardingFlow,
+} = require('../helpers');
+const enLocaleMessages = require('../../../app/_locales/en/messages.json');
describe('Add account', function () {
+ const testSeedPhrase =
+ 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress';
+ const testPassword = 'correct horse battery staple';
const ganacheOptions = {
accounts: [
{
secretKey:
- '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
+ '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9',
balance: convertToHexValue(25000000000000000000),
},
],
};
+
it('should display correct new account name after create', async function () {
await withFixtures(
{
@@ -36,4 +46,213 @@ describe('Add account', function () {
},
);
});
+
+ it('should add the same account addresses when a secret recovery phrase is imported, the account is locked, and the same secret recovery phrase is imported again', async function () {
+ await withFixtures(
+ {
+ fixtures: 'onboarding',
+ ganacheOptions,
+ title: this.test.title,
+ failOnConsoleError: false,
+ },
+ async ({ driver }) => {
+ await driver.navigate();
+
+ await completeImportSRPOnboardingFlow(
+ driver,
+ testSeedPhrase,
+ testPassword,
+ );
+
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Create Account', tag: 'div' });
+ await driver.fill('.new-account-create-form input', '2nd account');
+ await driver.clickElement({ text: 'Create', tag: 'button' });
+
+ await driver.clickElement(
+ '[data-testid="account-options-menu-button"]',
+ );
+ await driver.clickElement(
+ '[data-testid="account-options-menu__account-details"]',
+ );
+
+ const detailsModal = await driver.findVisibleElement('span .modal');
+ // get the public address for the "second account"
+ const secondAccountAddress = await driver.findElement(
+ '.qr-code__address',
+ );
+ const secondAccountPublicAddress = await secondAccountAddress.getText();
+
+ await driver.clickElement('.account-modal__close');
+ await detailsModal.waitForElementState('hidden');
+
+ // generate a third accound
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Create Account', tag: 'div' });
+ await driver.fill('.new-account-create-form input', '3rd account');
+ await driver.clickElement({ text: 'Create', tag: 'button' });
+
+ await driver.clickElement(
+ '[data-testid="account-options-menu-button"]',
+ );
+ await driver.clickElement(
+ '[data-testid="account-options-menu__account-details"]',
+ );
+
+ // get the public address for the "third account"
+ const secondDetailsModal = await driver.findVisibleElement(
+ 'span .modal',
+ );
+ const thirdAccountAddress = await driver.findElement(
+ '.qr-code__address',
+ );
+ const thirdAccountPublicAddress = await thirdAccountAddress.getText();
+
+ await driver.clickElement('.account-modal__close');
+ await secondDetailsModal.waitForElementState('hidden');
+
+ // lock account
+ await driver.clickElement('.account-menu__icon');
+ await driver.delay(regularDelayMs);
+
+ const lockButton = await driver.findClickableElement(
+ '.account-menu__lock-button',
+ );
+ await lockButton.click();
+ await driver.delay(regularDelayMs);
+
+ // restore same seed phrase
+ const restoreSeedLink = await driver.findClickableElement(
+ '.unlock-page__link',
+ );
+
+ await restoreSeedLink.click();
+ await driver.delay(regularDelayMs);
+
+ await driver.fill(
+ 'input[placeholder="Enter your Secret Recovery Phrase"]',
+ testSeedPhrase,
+ );
+ await driver.delay(regularDelayMs);
+
+ await driver.fill('#password', 'correct horse battery staple');
+ await driver.fill('#confirm-password', 'correct horse battery staple');
+ await driver.clickElement({
+ text: enLocaleMessages.restore.message,
+ tag: 'button',
+ });
+ await driver.delay(regularDelayMs);
+
+ // recreate a "2nd account"
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Create Account', tag: 'div' });
+ await driver.fill('.new-account-create-form input', '2nd account');
+ await driver.clickElement({ text: 'Create', tag: 'button' });
+
+ await driver.clickElement(
+ '[data-testid="account-options-menu-button"]',
+ );
+ await driver.clickElement(
+ '[data-testid="account-options-menu__account-details"]',
+ );
+ const thirdDetailsModal = await driver.findVisibleElement(
+ 'span .modal',
+ );
+ // get the public address for the "second account"
+ const recreatedSecondAccountAddress = await driver.findElement(
+ '.qr-code__address',
+ );
+
+ assert.equal(
+ await recreatedSecondAccountAddress.getText(),
+ secondAccountPublicAddress,
+ );
+
+ await driver.clickElement('.account-modal__close');
+ await thirdDetailsModal.waitForElementState('hidden');
+
+ // re-generate a third accound
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Create Account', tag: 'div' });
+ await driver.fill('.new-account-create-form input', '3rd account');
+ await driver.clickElement({ text: 'Create', tag: 'button' });
+
+ await driver.clickElement(
+ '[data-testid="account-options-menu-button"]',
+ );
+ await driver.clickElement(
+ '[data-testid="account-options-menu__account-details"]',
+ );
+
+ // get the public address for the "third account"
+ const recreatedThirdAccountAddress = await driver.findElement(
+ '.qr-code__address',
+ );
+ assert.strictEqual(
+ await recreatedThirdAccountAddress.getText(),
+ thirdAccountPublicAddress,
+ );
+ },
+ );
+ });
+
+ it('It should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding', async function () {
+ const testPrivateKey =
+ '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6';
+
+ await withFixtures(
+ {
+ fixtures: 'imported-account',
+ ganacheOptions,
+ title: this.test.title,
+ },
+ async ({ driver }) => {
+ await driver.navigate();
+ await driver.fill('#password', 'correct horse battery staple');
+ await driver.press('#password', driver.Key.ENTER);
+
+ await driver.delay(regularDelayMs);
+
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Create Account', tag: 'div' });
+ await driver.fill('.new-account-create-form input', '2nd account');
+ await driver.clickElement({ text: 'Create', tag: 'button' });
+
+ await driver.clickElement(
+ '[data-testid="account-options-menu-button"]',
+ );
+
+ const menuItems = await driver.findElements('.menu-item');
+ assert.equal(menuItems.length, 3);
+
+ // click out of menu
+ await driver.clickElement('.menu__background');
+
+ // import with private key
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Import Account', tag: 'div' });
+
+ // enter private key',
+ await driver.fill('#private-key-box', testPrivateKey);
+ await driver.clickElement({ text: 'Import', tag: 'button' });
+
+ // should show the correct account name
+ const importedAccountName = await driver.findElement(
+ '.selected-account__name',
+ );
+ assert.equal(await importedAccountName.getText(), 'Account 3');
+
+ await driver.clickElement(
+ '[data-testid="account-options-menu-button"]',
+ );
+
+ const menuItems2 = await driver.findElements('.menu-item');
+ assert.equal(menuItems2.length, 4);
+
+ await driver.findElement(
+ '[data-testid="account-options-menu__remove-account"]',
+ );
+ },
+ );
+ });
});
diff --git a/test/e2e/tests/add-hide-token.spec.js b/test/e2e/tests/add-hide-token.spec.js
index 56fb2840c..066bd8782 100644
--- a/test/e2e/tests/add-hide-token.spec.js
+++ b/test/e2e/tests/add-hide-token.spec.js
@@ -55,6 +55,7 @@ describe('Hide token', function () {
});
});
+/* eslint-disable-next-line mocha/max-top-level-suites */
describe('Add existing token using search', function () {
const ganacheOptions = {
accounts: [
diff --git a/test/e2e/tests/custom-rpc-history.spec.js b/test/e2e/tests/custom-rpc-history.spec.js
index 025f7f53a..49dd59059 100644
--- a/test/e2e/tests/custom-rpc-history.spec.js
+++ b/test/e2e/tests/custom-rpc-history.spec.js
@@ -36,10 +36,10 @@ describe('Stores custom RPC history', function () {
await driver.findElement('.networks-tab__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]');
- const networkNameInput = customRpcInputs[0];
- const rpcUrlInput = customRpcInputs[1];
- const chainIdInput = customRpcInputs[2];
- const symbolInput = customRpcInputs[3];
+ const networkNameInput = customRpcInputs[1];
+ const rpcUrlInput = customRpcInputs[2];
+ const chainIdInput = customRpcInputs[3];
+ const symbolInput = customRpcInputs[4];
await networkNameInput.clear();
await networkNameInput.sendKeys(networkName);
@@ -84,7 +84,7 @@ describe('Stores custom RPC history', function () {
await driver.findElement('.networks-tab__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]');
- const rpcUrlInput = customRpcInputs[1];
+ const rpcUrlInput = customRpcInputs[2];
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(duplicateRpcUrl);
@@ -120,8 +120,8 @@ describe('Stores custom RPC history', function () {
await driver.findElement('.networks-tab__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]');
- const rpcUrlInput = customRpcInputs[1];
- const chainIdInput = customRpcInputs[2];
+ const rpcUrlInput = customRpcInputs[2];
+ const chainIdInput = customRpcInputs[3];
await chainIdInput.clear();
await chainIdInput.sendKeys(duplicateChainId);
diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js
index a1a8e1da4..3157e6a43 100644
--- a/test/e2e/tests/from-import-ui.spec.js
+++ b/test/e2e/tests/from-import-ui.spec.js
@@ -4,8 +4,8 @@ const {
withFixtures,
regularDelayMs,
largeDelayMs,
+ completeImportSRPOnboardingFlow,
} = require('../helpers');
-const enLocaleMessages = require('../../../app/_locales/en/messages.json');
describe('Metamask Import UI', function () {
it('Importing wallet using Secret Recovery Phrase', async function () {
@@ -20,6 +20,7 @@ describe('Metamask Import UI', function () {
};
const testSeedPhrase =
'forum vessel pink push lonely enact gentle tail admit parrot grunt dress';
+ const testPassword = 'correct horse battery staple';
const testAddress = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3';
await withFixtures(
@@ -32,74 +33,11 @@ describe('Metamask Import UI', function () {
async ({ driver }) => {
await driver.navigate();
- if (process.env.ONBOARDING_V2 === '1') {
- // welcome
- await driver.clickElement('[data-testid="onboarding-import-wallet"]');
-
- // metrics
- await driver.clickElement('[data-testid="metametrics-no-thanks"]');
-
- // import with recovery phrase
- await driver.fill('[data-testid="import-srp-text"]', testSeedPhrase);
- await driver.clickElement('[data-testid="import-srp-confirm"]');
-
- // create password
- await driver.fill(
- '[data-testid="create-password-new"]',
- 'correct horse battery staple',
- );
- await driver.fill(
- '[data-testid="create-password-confirm"]',
- 'correct horse battery staple',
- );
- await driver.clickElement('[data-testid="create-password-terms"]');
- await driver.clickElement('[data-testid="create-password-import"]');
-
- // complete
- await driver.clickElement('[data-testid="onboarding-complete-done"]');
-
- // pin extension
- await driver.clickElement('[data-testid="pin-extension-next"]');
- await driver.clickElement('[data-testid="pin-extension-done"]');
- } else {
- // clicks the continue button on the welcome screen
- await driver.findElement('.welcome-page__header');
- await driver.clickElement({
- text: enLocaleMessages.getStarted.message,
- tag: 'button',
- });
-
- // clicks the "Import Wallet" option
- await driver.clickElement({ text: 'Import wallet', tag: 'button' });
-
- // clicks the "No thanks" option on the metametrics opt-in screen
- await driver.clickElement('.btn-secondary');
-
- // Import Secret Recovery Phrase
- await driver.fill(
- 'input[placeholder="Enter your Secret Recovery Phrase"]',
- testSeedPhrase,
- );
-
- await driver.fill('#password', 'correct horse battery staple');
- await driver.fill(
- '#confirm-password',
- 'correct horse battery staple',
- );
-
- await driver.clickElement(
- '[data-testid="create-new-vault__terms-checkbox"]',
- );
-
- await driver.clickElement({ text: 'Import', tag: 'button' });
-
- // clicks through the success screen
- await driver.findElement({ text: 'Congratulations', tag: 'div' });
- await driver.clickElement({
- text: enLocaleMessages.endOfFlowMessage10.message,
- tag: 'button',
- });
- }
+ await completeImportSRPOnboardingFlow(
+ driver,
+ testSeedPhrase,
+ testPassword,
+ );
// Show account information
await driver.clickElement(
@@ -293,6 +231,47 @@ describe('Metamask Import UI', function () {
},
);
});
+
+ it('Import Account using private key of an already active account should result in an error', async function () {
+ const testPrivateKey =
+ '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9';
+ const ganacheOptions = {
+ accounts: [
+ {
+ secretKey: testPrivateKey,
+ balance: convertToHexValue(25000000000000000000),
+ },
+ ],
+ };
+
+ await withFixtures(
+ {
+ fixtures: 'import-ui',
+ ganacheOptions,
+ title: this.test.title,
+ },
+ async ({ driver }) => {
+ await driver.navigate();
+ await driver.fill('#password', 'correct horse battery staple');
+ await driver.press('#password', driver.Key.ENTER);
+
+ // choose Import Account from the account menu
+ await driver.clickElement('.account-menu__icon');
+ await driver.clickElement({ text: 'Import Account', tag: 'div' });
+
+ // enter private key',
+ await driver.fill('#private-key-box', testPrivateKey);
+ await driver.clickElement({ text: 'Import', tag: 'button' });
+
+ // error should occur
+ await driver.waitForSelector({
+ css: '.error',
+ text: "The account you're are trying to import is a duplicate",
+ });
+ },
+ );
+ });
+
it('Connects to a Hardware wallet', async function () {
const ganacheOptions = {
accounts: [
diff --git a/test/e2e/tests/metamask-responsive-ui.spec.js b/test/e2e/tests/metamask-responsive-ui.spec.js
index 3bf8684fb..e01e5b307 100644
--- a/test/e2e/tests/metamask-responsive-ui.spec.js
+++ b/test/e2e/tests/metamask-responsive-ui.spec.js
@@ -164,12 +164,9 @@ describe('Metamask Responsive UI', function () {
// Import Secret Recovery Phrase
const restoreSeedLink = await driver.findClickableElement(
- '.unlock-page__link--import',
- );
- assert.equal(
- await restoreSeedLink.getText(),
- 'import using Secret Recovery Phrase',
+ '.unlock-page__link',
);
+ assert.equal(await restoreSeedLink.getText(), 'Forgot password?');
await restoreSeedLink.click();
await driver.fill(
diff --git a/test/e2e/tests/phishing-detection.spec.js b/test/e2e/tests/phishing-detection.spec.js
new file mode 100644
index 000000000..0cacf1e23
--- /dev/null
+++ b/test/e2e/tests/phishing-detection.spec.js
@@ -0,0 +1,53 @@
+const { strict: assert } = require('assert');
+const { convertToHexValue, withFixtures } = require('../helpers');
+
+describe('Phishing Detection', function () {
+ function mockPhishingDetection(mockServer) {
+ mockServer
+ .forGet(
+ 'https://cdn.jsdelivr.net/gh/MetaMask/eth-phishing-detect@master/src/config.json',
+ )
+ .thenCallback(() => {
+ return {
+ headers: { 'Access-Control-Allow-Origin': '*' },
+ statusCode: 200,
+ json: {
+ version: 2,
+ tolerance: 2,
+ fuzzylist: [],
+ whitelist: [],
+ blacklist: ['example.com'],
+ },
+ };
+ });
+ }
+ const ganacheOptions = {
+ accounts: [
+ {
+ secretKey:
+ '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
+ balance: convertToHexValue(25000000000000000000),
+ },
+ ],
+ };
+ it('should display the MetaMask Phishing Detection page', async function () {
+ await withFixtures(
+ {
+ fixtures: 'imported-account',
+ ganacheOptions,
+ title: this.test.title,
+ testSpecificMock: mockPhishingDetection,
+ },
+ async ({ driver }) => {
+ await driver.navigate();
+ await driver.fill('#password', 'correct horse battery staple');
+ await driver.press('#password', driver.Key.ENTER);
+ await driver.navigate();
+ await driver.openNewPage('http://example.com');
+ await driver.waitForSelector({ text: 'continuing at your own risk' });
+ const header = await driver.findElement('h1');
+ assert.equal(await header.getText(), 'MetaMask Phishing Detection');
+ },
+ );
+ });
+});
diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js
index 7c12e321b..f17f116d9 100644
--- a/test/e2e/tests/send-eth.spec.js
+++ b/test/e2e/tests/send-eth.spec.js
@@ -91,6 +91,7 @@ describe('Send ETH from inside MetaMask using default gas', function () {
});
});
+/* eslint-disable-next-line mocha/max-top-level-suites */
describe('Send ETH from inside MetaMask using advanced gas modal', function () {
const ganacheOptions = {
accounts: [
diff --git a/test/e2e/tests/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js
index 335826069..7cb53e233 100644
--- a/test/e2e/tests/signature-request.spec.js
+++ b/test/e2e/tests/signature-request.spec.js
@@ -86,6 +86,7 @@ describe('Sign Typed Data V4 Signature Request', function () {
});
});
+/* eslint-disable-next-line mocha/max-top-level-suites */
describe('Sign Typed Data V3 Signature Request', function () {
it('can initiate and confirm a Signature Request', async function () {
const ganacheOptions = {
diff --git a/test/helpers/setup-helper.js b/test/helpers/setup-helper.js
index f9d2112aa..9d4b7d892 100644
--- a/test/helpers/setup-helper.js
+++ b/test/helpers/setup-helper.js
@@ -79,3 +79,11 @@ if (!window.crypto.getRandomValues) {
// eslint-disable-next-line node/global-require
window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues');
}
+
+// Used to test `clearClipboard` function
+if (!window.navigator.clipboard) {
+ window.navigator.clipboard = {};
+}
+if (!window.navigator.clipboard.writeText) {
+ window.navigator.clipboard.writeText = () => undefined;
+}
diff --git a/test/lib/render-helpers.js b/test/lib/render-helpers.js
index 37c5d6370..9fc7aad16 100644
--- a/test/lib/render-helpers.js
+++ b/test/lib/render-helpers.js
@@ -95,3 +95,17 @@ export function renderWithProvider(component, store) {
return render(component, { wrapper: Wrapper });
}
+
+export function renderWithLocalization(component) {
+ const Wrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ Wrapper.propTypes = {
+ children: PropTypes.node,
+ };
+
+ return render(component, { wrapper: Wrapper });
+}
diff --git a/ui/components/app/account-menu/account-menu.component.js b/ui/components/app/account-menu/account-menu.component.js
index 8105af35b..4c3ff209c 100644
--- a/ui/components/app/account-menu/account-menu.component.js
+++ b/ui/components/app/account-menu/account-menu.component.js
@@ -125,7 +125,7 @@ export default class AccountMenu extends Component {
marginLeft: '8px',
}}
>
-
+
);
diff --git a/ui/components/app/account-menu/keyring-label.js b/ui/components/app/account-menu/keyring-label.js
index 794eed5e4..65007390c 100644
--- a/ui/components/app/account-menu/keyring-label.js
+++ b/ui/components/app/account-menu/keyring-label.js
@@ -22,7 +22,7 @@ export default function KeyRingLabel({ keyring }) {
case KEYRING_TYPES.QR:
label = KEYRING_NAMES.QR;
break;
- case 'Simple Key Pair':
+ case KEYRING_TYPES.IMPORTED:
label = t('imported');
break;
case KEYRING_TYPES.TREZOR:
diff --git a/ui/components/app/add-network/add-network.js b/ui/components/app/add-network/add-network.js
new file mode 100644
index 000000000..6f9ba8c9e
--- /dev/null
+++ b/ui/components/app/add-network/add-network.js
@@ -0,0 +1,133 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { I18nContext } from '../../../contexts/i18n';
+import ActionableMessage from '../../ui/actionable-message';
+import Box from '../../ui/box';
+import Typography from '../../ui/typography';
+import {
+ ALIGN_ITEMS,
+ BLOCK_SIZES,
+ COLORS,
+ DISPLAY,
+ FLEX_DIRECTION,
+ TYPOGRAPHY,
+} from '../../../helpers/constants/design-system';
+import Button from '../../ui/button';
+
+const AddNetwork = ({
+ onBackClick,
+ onAddNetworkClick,
+ onAddNetworkManuallyClick,
+ featuredRPCS,
+}) => {
+ const t = useContext(I18nContext);
+
+ const nets = featuredRPCS
+ .sort((a, b) => (a.ticker > b.ticker ? 1 : -1))
+ .slice(0, 5);
+
+ return (
+
+
+
+
+ {t('addNetwork')}
+
+
+
+
+ {t('addFromAListOfPopularNetworks')}
+
+
+ {t('customNetworks')}
+
+ {nets.map((item, index) => (
+
+
+
+ {item.ticker}
+
+
+
+ ))}
+
+
+
+
+ {t('addANetworkManually')}
+
+
+
+ {t('onlyInteractWith')}
+
+ {t('endOfFlowMessage9')}
+
+ >
+ }
+ iconFillColor="#f8c000"
+ useIcon
+ withRightButton
+ />
+
+
+ );
+};
+
+AddNetwork.propTypes = {
+ onBackClick: PropTypes.func,
+ onAddNetworkClick: PropTypes.func,
+ onAddNetworkManuallyClick: PropTypes.func,
+ featuredRPCS: PropTypes.array,
+};
+
+export default AddNetwork;
diff --git a/ui/components/app/add-network/add-network.stories.js b/ui/components/app/add-network/add-network.stories.js
new file mode 100644
index 000000000..5dbcc6bee
--- /dev/null
+++ b/ui/components/app/add-network/add-network.stories.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import AddNetwork from '.';
+
+export default {
+ title: 'Components/APP/AddNetwork',
+ id: __filename,
+};
+
+export const DefaultStory = () => {
+ const MATIC_TOKEN_IMAGE_URL = './images/matic-token.png';
+ const ARBITRUM_IMAGE_URL = './images/arbitrum.svg';
+ const OPTIMISM_IMAGE_URL = './images/optimism.svg';
+
+ const FEATURED_RPCS = [
+ {
+ chainId: '0x89',
+ nickname: 'Polygon Mumbai',
+ rpcUrl:
+ 'https://polygon-mainnet.infura.io/v3/2b6d4a83d89a438eb1b5d036788ab29c',
+ ticker: 'MATIC',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://mumbai.polygonscan.com/',
+ imageUrl: MATIC_TOKEN_IMAGE_URL,
+ },
+ },
+ {
+ chainId: '0x99',
+ nickname: 'Optimism Testnet ',
+ rpcUrl:
+ 'https://optimism-kovan.infura.io/v3/2b6d4a83d89a438eb1b5d036788ab29c',
+ ticker: 'KOR',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://kovan-optimistic.etherscan.io/',
+ imageUrl: OPTIMISM_IMAGE_URL,
+ },
+ },
+ {
+ chainId: '0x66eeb',
+ nickname: 'Arbitrum Testnet',
+ rpcUrl:
+ 'https://arbitrum-rinkeby.infura.io/v3/2b6d4a83d89a438eb1b5d036788ab29c',
+ ticker: 'ARETH',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://testnet.arbiscan.io/',
+ imageUrl: ARBITRUM_IMAGE_URL,
+ },
+ },
+ ];
+
+ return ;
+};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/add-network/index.js b/ui/components/app/add-network/index.js
new file mode 100644
index 000000000..8727c8809
--- /dev/null
+++ b/ui/components/app/add-network/index.js
@@ -0,0 +1 @@
+export { default } from './add-network';
diff --git a/ui/components/app/add-network/index.scss b/ui/components/app/add-network/index.scss
new file mode 100644
index 000000000..b62eebfe4
--- /dev/null
+++ b/ui/components/app/add-network/index.scss
@@ -0,0 +1,42 @@
+.add-network {
+ &__header {
+ border-bottom: 1px solid var(--ui-grey);
+
+ &__back-icon {
+ padding-left: 24px;
+ padding-right: 26px;
+ }
+ }
+
+ &__token-image {
+ margin-right: 7px;
+ height: 24px;
+ width: 24px;
+ }
+
+ &__add-icon {
+ height: 16px;
+ width: 12px;
+ color: var(--ui-4);
+ margin-left: auto;
+ margin-right: 0;
+ }
+
+ &__footer {
+ border-top: 1px solid var(--ui-2);
+
+ & .btn-link {
+ display: initial;
+ padding: 0;
+ }
+
+ &__link {
+ color: var(--primary-1);
+ }
+
+ & .actionable-message--warning .actionable-message__message,
+ .actionable-message--warning .actionable-message__action {
+ color: var(--ui-4);
+ }
+ }
+}
diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js
index cf8268354..539c43c36 100644
--- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js
+++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js
@@ -60,24 +60,24 @@ describe('BaseFeeInput', () => {
it('should renders advancedGasFee.baseFee value if current estimate used is not custom', () => {
render({
userFeeLevel: 'high',
- txParams: {
- maxFeePerGas: '0x2E90EDD000',
- },
});
expect(document.getElementsByTagName('input')[0]).toHaveValue(100);
});
- it('should not advancedGasFee.baseFee value for swaps', () => {
+ it('should not use advancedGasFee.baseFee value for swaps', () => {
render(
{
userFeeLevel: 'high',
- txParams: {
- maxFeePerGas: '0x2E90EDD000',
- },
},
{ editGasMode: EDIT_GAS_MODES.SWAPS },
);
- expect(document.getElementsByTagName('input')[0]).toHaveValue(200);
+ expect(document.getElementsByTagName('input')[0]).toHaveValue(
+ parseInt(
+ mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates.high
+ .suggestedMaxFeePerGas,
+ 10,
+ ),
+ );
});
it('should renders baseFee values from transaction if current estimate used is custom', () => {
@@ -89,19 +89,11 @@ describe('BaseFeeInput', () => {
expect(document.getElementsByTagName('input')[0]).toHaveValue(200);
});
it('should show current value of estimatedBaseFee in subtext', () => {
- render({
- txParams: {
- maxFeePerGas: '0x174876E800',
- },
- });
+ render();
expect(screen.queryByText('50 GWEI')).toBeInTheDocument();
});
it('should show 12hr range value in subtext', () => {
- render({
- txParams: {
- maxFeePerGas: '0x174876E800',
- },
- });
+ render();
expect(screen.queryByText('50 - 100 GWEI')).toBeInTheDocument();
});
it('should show error if base fee is less than suggested low value', () => {
@@ -120,7 +112,6 @@ describe('BaseFeeInput', () => {
target: { value: 50 },
});
});
-
it('should show error if base if is more than suggested high value', () => {
render({
txParams: {
diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js
index 91b875ea8..686f5d65c 100644
--- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js
+++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js
@@ -60,26 +60,24 @@ describe('PriorityfeeInput', () => {
it('should renders advancedGasFee.priorityfee value if current estimate used is not custom', () => {
render({
userFeeLevel: 'high',
- txParams: {
- maxFeePerGas: '0x2E90EDD000',
- },
});
expect(document.getElementsByTagName('input')[0]).toHaveValue(100);
});
-
- it('should not advancedGasFee.baseFee value for swaps', () => {
+ it('should not use advancedGasFee.priorityfee value for swaps', () => {
render(
{
userFeeLevel: 'high',
- txParams: {
- maxFeePerGas: '0x2E90EDD000',
- },
},
{ editGasMode: EDIT_GAS_MODES.SWAPS },
);
- expect(document.getElementsByTagName('input')[0]).toHaveValue(200);
+ expect(document.getElementsByTagName('input')[0]).toHaveValue(
+ parseInt(
+ mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates.high
+ .suggestedMaxPriorityFeePerGas,
+ 10,
+ ),
+ );
});
-
it('should renders priorityfee value from transaction if current estimate used is custom', () => {
render({
txParams: {
@@ -89,19 +87,11 @@ describe('PriorityfeeInput', () => {
expect(document.getElementsByTagName('input')[0]).toHaveValue(2);
});
it('should show current priority fee range in subtext', () => {
- render({
- txParams: {
- maxFeePerGas: '0x174876E800',
- },
- });
+ render();
expect(screen.queryByText('1 - 20 GWEI')).toBeInTheDocument();
});
it('should show 12hr range value in subtext', () => {
- render({
- txParams: {
- maxFeePerGas: '0x174876E800',
- },
- });
+ render();
expect(screen.queryByText('2 - 125 GWEI')).toBeInTheDocument();
});
it('should show error if value entered is 0', () => {
diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss
index c11ff6027..22b06bc70 100644
--- a/ui/components/app/app-components.scss
+++ b/ui/components/app/app-components.scss
@@ -1,6 +1,7 @@
/** Please import your files in alphabetical order **/
@import 'account-list-item/index';
@import 'account-menu/index';
+@import 'add-network/index';
@import 'app-loading-spinner/index';
@import 'import-token-link/index';
@import 'advanced-gas-controls/index';
@@ -54,6 +55,7 @@
@import 'selected-account/index';
@import 'signature-request/index';
@import 'signature-request-original/index';
+@import 'srp-input/srp-input';
@import 'tab-bar/index';
@import 'token-cell/token-cell';
@import 'token-list-display/token-list-display';
diff --git a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js
index 92fa00dc4..da2025e12 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js
+++ b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js
@@ -42,6 +42,7 @@ describe('Confirm Page Container Container Test', () => {
identities: [],
featureFlags: {},
enableEIP1559V2NoticeDismissed: true,
+ tokenList: {},
},
};
diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
index 665d1924f..16f89f91e 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
+++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
@@ -2,9 +2,16 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Tabs, Tab } from '../../../ui/tabs';
-import ErrorMessage from '../../../ui/error-message';
+import Button from '../../../ui/button';
import ActionableMessage from '../../../ui/actionable-message/actionable-message';
import { PageContainerFooter } from '../../../ui/page-container';
+import ErrorMessage from '../../../ui/error-message';
+import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../../helpers/constants/error-keys';
+import Typography from '../../../ui/typography';
+import { TYPOGRAPHY } from '../../../../helpers/constants/design-system';
+import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction';
+import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network';
+
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.';
export default class ConfirmPageContainerContent extends Component {
@@ -21,7 +28,7 @@ export default class ConfirmPageContainerContent extends Component {
errorMessage: PropTypes.string,
hasSimulationError: PropTypes.bool,
hideSubtitle: PropTypes.bool,
- identiconAddress: PropTypes.string,
+ tokenAddress: PropTypes.string,
nonce: PropTypes.string,
subtitleComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -44,6 +51,12 @@ export default class ConfirmPageContainerContent extends Component {
hideTitle: PropTypes.bool,
supportsEIP1559V2: PropTypes.bool,
hasTopBorder: PropTypes.bool,
+ currentTransaction: PropTypes.string,
+ nativeCurrency: PropTypes.string,
+ networkName: PropTypes.string,
+ showBuyModal: PropTypes.func,
+ toAddress: PropTypes.string,
+ transactionType: PropTypes.string,
};
renderContent() {
@@ -93,7 +106,7 @@ export default class ConfirmPageContainerContent extends Component {
titleComponent,
subtitleComponent,
hideSubtitle,
- identiconAddress,
+ tokenAddress,
nonce,
detailsComponent,
dataComponent,
@@ -113,6 +126,12 @@ export default class ConfirmPageContainerContent extends Component {
hideUserAcknowledgedGasMissing,
supportsEIP1559V2,
hasTopBorder,
+ currentTransaction,
+ nativeCurrency,
+ networkName,
+ showBuyModal,
+ toAddress,
+ transactionType,
} = this.props;
const primaryAction = hideUserAcknowledgedGasMissing
@@ -121,6 +140,14 @@ export default class ConfirmPageContainerContent extends Component {
label: this.context.t('tryAnywayOption'),
onClick: setUserAcknowledgedGasMissing,
};
+ const { t } = this.context;
+
+ const showInsuffienctFundsError =
+ supportsEIP1559V2 &&
+ !hasSimulationError &&
+ (errorKey || errorMessage) &&
+ errorKey === INSUFFICIENT_FUNDS_ERROR_KEY &&
+ currentTransaction.type === TRANSACTION_TYPES.SIMPLE_SEND;
return (
)}
@@ -152,19 +179,63 @@ export default class ConfirmPageContainerContent extends Component {
titleComponent={titleComponent}
subtitleComponent={subtitleComponent}
hideSubtitle={hideSubtitle}
- identiconAddress={identiconAddress}
+ tokenAddress={tokenAddress}
nonce={nonce}
origin={origin}
hideTitle={hideTitle}
+ toAddress={toAddress}
+ transactionType={transactionType}
/>
{this.renderContent()}
{!supportsEIP1559V2 &&
!hasSimulationError &&
- (errorKey || errorMessage) && (
+ (errorKey || errorMessage) &&
+ currentTransaction.type !== TRANSACTION_TYPES.SIMPLE_SEND && (
)}
+ {showInsuffienctFundsError && (
+
+ {currentTransaction.chainId === MAINNET_CHAIN_ID ? (
+
+ {t('insufficientCurrency', [nativeCurrency, networkName])}
+
+ {t('buyEth')}
+
+
+ {t('orDeposit')}
+
+ }
+ useIcon
+ iconFillColor="#d73a49"
+ type="danger"
+ />
+ ) : (
+
+ {t('insufficientCurrency', [nativeCurrency, networkName])}
+ {t('buyOther', [nativeCurrency])}
+
+ }
+ useIcon
+ iconFillColor="#d73a49"
+ type="danger"
+ />
+ )}
+
+ )}
+
{
metamask: {
provider: {
type: 'test',
+ chainId: '0x3',
},
eip1559V2Enabled: false,
+ addressBook: {
+ '0x3': {
+ '0x06195827297c7A80a443b6894d3BDB8824b43896': {
+ address: '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ name: 'Address Book Account 1',
+ chainId: '0x3',
+ },
+ },
+ },
},
};
@@ -75,6 +86,9 @@ describe('Confirm Page Container Content', () => {
props.hasSimulationError = false;
props.disabled = true;
props.errorKey = TRANSACTION_ERROR_KEY;
+ props.currentTransaction = {
+ type: 'transfer',
+ };
const { queryByText, getByText } = renderWithProvider(
,
store,
@@ -122,4 +136,30 @@ describe('Confirm Page Container Content', () => {
fireEvent.click(cancelButton);
expect(props.onCancel).toHaveBeenCalledTimes(1);
});
+
+ it('render contract address name from addressBook in title for contract', async () => {
+ props.hasSimulationError = false;
+ props.disabled = false;
+ props.toAddress = '0x06195827297c7A80a443b6894d3BDB8824b43896';
+ props.transactionType = TRANSACTION_TYPES.CONTRACT_INTERACTION;
+ const { queryByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ expect(queryByText('Address Book Account 1')).toBeInTheDocument();
+ });
+
+ it('render simple title without address name for simple send', async () => {
+ props.hasSimulationError = false;
+ props.disabled = false;
+ props.toAddress = '0x06195827297c7A80a443b6894d3BDB8824b43896';
+ props.transactionType = TRANSACTION_TYPES.SIMPLE_SEND;
+ const { queryByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ expect(queryByText('Address Book Account 1')).not.toBeInTheDocument();
+ });
});
diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
index d4878442f..089b359b8 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
+++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
@@ -1,8 +1,16 @@
/* eslint-disable no-negated-condition */
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
+
+import { TRANSACTION_TYPES } from '../../../../../../shared/constants/transaction';
+import { toChecksumHexAddress } from '../../../../../../shared/modules/hexstring-utils';
+import { useI18nContext } from '../../../../../hooks/useI18nContext';
+import useAddressDetails from '../../../../../hooks/useAddressDetails';
+
import Identicon from '../../../../ui/identicon';
+import InfoTooltip from '../../../../ui/info-tooltip';
+import NicknamePopovers from '../../../modals/nickname-popovers';
const ConfirmPageContainerSummary = (props) => {
const {
@@ -12,13 +20,41 @@ const ConfirmPageContainerSummary = (props) => {
subtitleComponent,
hideSubtitle,
className,
- identiconAddress,
+ tokenAddress,
+ toAddress,
nonce,
origin,
hideTitle,
image,
+ transactionType,
} = props;
+ const [showNicknamePopovers, setShowNicknamePopovers] = useState(false);
+ const t = useI18nContext();
+
+ const contractInitiatedTransactionType = [
+ TRANSACTION_TYPES.CONTRACT_INTERACTION,
+ TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
+ TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
+ ];
+ const isContractTypeTransaction = contractInitiatedTransactionType.includes(
+ transactionType,
+ );
+ let contractAddress;
+ if (isContractTypeTransaction) {
+ // If the transaction is TOKEN_METHOD_TRANSFER or TOKEN_METHOD_TRANSFER_FROM
+ // the contract address is passed down as tokenAddress, if it is anyother
+ // type of contract interaction it is passed as toAddress
+ contractAddress =
+ transactionType === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER ||
+ transactionType === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM
+ ? tokenAddress
+ : toAddress;
+ }
+
+ const { toName, isTrusted } = useAddressDetails(contractAddress);
+ const checksummedAddress = toChecksumHexAddress(contractAddress);
+
const renderImage = () => {
if (image) {
return (
@@ -28,12 +64,12 @@ const ConfirmPageContainerSummary = (props) => {
src={image}
/>
);
- } else if (identiconAddress) {
+ } else if (contractAddress) {
return (
);
@@ -47,7 +83,29 @@ const ConfirmPageContainerSummary = (props) => {
{origin}
)}
-
{action}
+
+ {isContractTypeTransaction && toName && (
+
+ setShowNicknamePopovers(true)}
+ role="button"
+ >
+ {toName}
+
+ :
+
+ )}
+
+ {action}
+
+ {isContractTypeTransaction && isTrusted === false && (
+
+ )}
+
{nonce && (
{`#${nonce}`}
@@ -69,6 +127,12 @@ const ConfirmPageContainerSummary = (props) => {
)}
>
+ {showNicknamePopovers && (
+
setShowNicknamePopovers(false)}
+ address={checksummedAddress}
+ />
+ )}
);
};
@@ -81,10 +145,12 @@ ConfirmPageContainerSummary.propTypes = {
subtitleComponent: PropTypes.node,
hideSubtitle: PropTypes.bool,
className: PropTypes.string,
- identiconAddress: PropTypes.string,
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
nonce: PropTypes.string,
origin: PropTypes.string.isRequired,
hideTitle: PropTypes.bool,
+ transactionType: PropTypes.string,
};
export default ConfirmPageContainerSummary;
diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
index d8f7a307c..0f4941976 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
+++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
@@ -23,12 +23,36 @@
&__action {
@include H7;
- text-transform: uppercase;
color: var(--oslo-gray);
padding: 3px 8px;
border: 1px solid var(--oslo-gray);
border-radius: 4px;
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
+
+ &__name {
+ text-transform: uppercase;
+ }
+
+ .info-tooltip {
+ margin-inline-start: 4px;
+
+ &__tooltip-container {
+ margin-bottom: -3px;
+ }
+ }
+
+ &__contract-address {
+ margin-inline-end: 4px;
+ }
+
+ &__contract-address-btn {
+ background: none;
+ border: none;
+ padding: 0;
+ margin-inline-end: 4px;
+ color: var(--primary-1);
+ }
}
&__nonce {
diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss
index b932af183..f52888db2 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss
+++ b/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss
@@ -100,4 +100,12 @@
&__total-value {
position: relative;
}
+
+ &__link {
+ background: transparent;
+ border: 0 transparent;
+ display: inline;
+ padding: 0;
+ font-size: $font-size-h7;
+ }
}
diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js
index 10f326594..641230df8 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container.component.js
+++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js
@@ -4,10 +4,15 @@ import PropTypes from 'prop-types';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
+import {
+ NETWORK_TO_NAME_MAP,
+ MAINNET_CHAIN_ID,
+} from '../../../../shared/constants/network';
import { PageContainerFooter } from '../../ui/page-container';
import Dialog from '../../ui/dialog';
-import ErrorMessage from '../../ui/error-message';
+import Button from '../../ui/button';
+import ActionableMessage from '../../ui/actionable-message/actionable-message';
import SenderToRecipient from '../../ui/sender-to-recipient';
import NicknamePopovers from '../modals/nickname-popovers';
@@ -15,6 +20,10 @@ import NicknamePopovers from '../modals/nickname-popovers';
import AdvancedGasFeePopover from '../advanced-gas-fee-popover';
import EditGasFeePopover from '../edit-gas-fee-popover/edit-gas-fee-popover';
import EditGasPopover from '../edit-gas-popover';
+import ErrorMessage from '../../ui/error-message';
+import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../helpers/constants/error-keys';
+import Typography from '../../ui/typography';
+import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
import EnableEIP1559V2Notice from './enableEIP1559V2-notice';
import {
@@ -58,7 +67,7 @@ export default class ConfirmPageContainer extends Component {
dataComponent: PropTypes.node,
dataHexComponent: PropTypes.node,
detailsComponent: PropTypes.node,
- identiconAddress: PropTypes.string,
+ tokenAddress: PropTypes.string,
nonce: PropTypes.string,
warning: PropTypes.string,
unapprovedTxCount: PropTypes.number,
@@ -87,6 +96,8 @@ export default class ConfirmPageContainer extends Component {
contact: PropTypes.object,
isOwnedAccount: PropTypes.bool,
supportsEIP1559V2: PropTypes.bool,
+ nativeCurrency: PropTypes.string,
+ showBuyModal: PropTypes.func,
};
render() {
@@ -115,7 +126,7 @@ export default class ConfirmPageContainer extends Component {
onCancelAll,
onCancel,
onSubmit,
- identiconAddress,
+ tokenAddress,
nonce,
unapprovedTxCount,
warning,
@@ -139,6 +150,8 @@ export default class ConfirmPageContainer extends Component {
contact = {},
isOwnedAccount,
supportsEIP1559V2,
+ nativeCurrency,
+ showBuyModal,
} = this.props;
const showAddToAddressDialog =
@@ -152,6 +165,10 @@ export default class ConfirmPageContainer extends Component {
currentTransaction.type === TRANSACTION_TYPES.DEPLOY_CONTRACT) &&
currentTransaction.txParams?.value === '0x0';
+ const networkName = NETWORK_TO_NAME_MAP[currentTransaction.chainId];
+
+ const { t } = this.context;
+
return (
@@ -192,7 +209,7 @@ export default class ConfirmPageContainer extends Component {
className="send__dialog"
onClick={() => this.setState({ showNicknamePopovers: true })}
>
- {this.context.t('newAccountDetectedDialogMessage')}
+ {t('newAccountDetectedDialogMessage')}
{this.state.showNicknamePopovers ? (
)}
- {shouldDisplayWarning && (
+ {shouldDisplayWarning && errorKey === INSUFFICIENT_FUNDS_ERROR_KEY && (
+
+ {currentTransaction.chainId === MAINNET_CHAIN_ID ? (
+
+ {t('insufficientCurrency', [nativeCurrency, networkName])}
+
+ {t('buyEth')}
+
+
+ {t('orDeposit')}
+
+ }
+ useIcon
+ iconFillColor="#d73a49"
+ type="danger"
+ />
+ ) : (
+
+ {t('insufficientCurrency', [nativeCurrency, networkName])}
+ {t('buyOther', [nativeCurrency])}
+
+ }
+ useIcon
+ iconFillColor="#d73a49"
+ type="danger"
+ />
+ )}
+
+ )}
+ {shouldDisplayWarning && errorKey !== INSUFFICIENT_FUNDS_ERROR_KEY && (
@@ -245,14 +309,14 @@ export default class ConfirmPageContainer extends Component {
{contentComponent && (
{unapprovedTxCount > 1 && (
- {this.context.t('rejectTxsN', [unapprovedTxCount])}
+ {t('rejectTxsN', [unapprovedTxCount])}
)}
diff --git a/ui/components/app/confirm-page-container/confirm-page-container.container.js b/ui/components/app/confirm-page-container/confirm-page-container.container.js
index ec5ba84c0..d0d287aed 100644
--- a/ui/components/app/confirm-page-container/confirm-page-container.container.js
+++ b/ui/components/app/confirm-page-container/confirm-page-container.container.js
@@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { getAccountsWithLabels, getAddressBookEntry } from '../../../selectors';
+import { showModal } from '../../../store/actions';
import ConfirmPageContainer from './confirm-page-container.component';
function mapStateToProps(state, ownProps) {
@@ -16,4 +17,13 @@ function mapStateToProps(state, ownProps) {
};
}
-export default connect(mapStateToProps)(ConfirmPageContainer);
+const mapDispatchToProps = (dispatch) => {
+ return {
+ showBuyModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(ConfirmPageContainer);
diff --git a/ui/components/app/confirm-page-container/index.scss b/ui/components/app/confirm-page-container/index.scss
index 9723c2d23..20aa1d94f 100644
--- a/ui/components/app/confirm-page-container/index.scss
+++ b/ui/components/app/confirm-page-container/index.scss
@@ -7,4 +7,12 @@
&__content-component-wrapper {
height: 100%;
}
+
+ &__link {
+ background: transparent;
+ border: 0 transparent;
+ display: inline;
+ padding: 0;
+ font-size: $font-size-h7;
+ }
}
diff --git a/ui/components/app/create-new-vault/create-new-vault.js b/ui/components/app/create-new-vault/create-new-vault.js
index 3c2b5007b..05ca7a716 100644
--- a/ui/components/app/create-new-vault/create-new-vault.js
+++ b/ui/components/app/create-new-vault/create-new-vault.js
@@ -1,17 +1,12 @@
-import { ethers } from 'ethers';
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import TextField from '../../ui/text-field';
import Button from '../../ui/button';
-import { clearClipboard } from '../../../helpers/utils/util';
import CheckBox from '../../ui/check-box';
import Typography from '../../ui/typography';
-import { COLORS } from '../../../helpers/constants/design-system';
-import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase';
-
-const { isValidMnemonic } = ethers.utils;
+import SrpInput from '../srp-input';
export default function CreateNewVault({
disabled = false,
@@ -24,33 +19,11 @@ export default function CreateNewVault({
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [seedPhrase, setSeedPhrase] = useState('');
- const [seedPhraseError, setSeedPhraseError] = useState('');
- const [showSeedPhrase, setShowSeedPhrase] = useState(false);
const [termsChecked, setTermsChecked] = useState(false);
const t = useI18nContext();
const metricsEvent = useContext(MetaMetricsContext);
- const onSeedPhraseChange = useCallback(
- (rawSeedPhrase) => {
- let newSeedPhraseError = '';
-
- if (rawSeedPhrase) {
- const parsedSeedPhrase = parseSecretRecoveryPhrase(rawSeedPhrase);
- const wordCount = parsedSeedPhrase.split(/\s/u).length;
- if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) {
- newSeedPhraseError = t('seedPhraseReq');
- } else if (!isValidMnemonic(parsedSeedPhrase)) {
- newSeedPhraseError = t('invalidSeedPhrase');
- }
- }
-
- setSeedPhrase(rawSeedPhrase);
- setSeedPhraseError(newSeedPhraseError);
- },
- [setSeedPhrase, setSeedPhraseError, t],
- );
-
const onPasswordChange = useCallback(
(newPassword) => {
let newConfirmPasswordError = '';
@@ -93,8 +66,7 @@ export default function CreateNewVault({
seedPhrase &&
(!includeTerms || termsChecked) &&
!passwordError &&
- !confirmPasswordError &&
- !seedPhraseError;
+ !confirmPasswordError;
const onImport = useCallback(
async (event) => {
@@ -104,7 +76,7 @@ export default function CreateNewVault({
return;
}
- await onSubmit(password, parseSecretRecoveryPhrase(seedPhrase));
+ await onSubmit(password, seedPhrase);
},
[isValid, onSubmit, password, seedPhrase],
);
@@ -121,10 +93,6 @@ export default function CreateNewVault({
setTermsChecked((currentTermsChecked) => !currentTermsChecked);
}, [metricsEvent]);
- const toggleShowSeedPhrase = useCallback(() => {
- setShowSeedPhrase((currentShowSeedPhrase) => !currentShowSeedPhrase);
- }, []);
-
const termsOfUse = t('acceptTermsOfUse', [
-
- {t('secretRecoveryPhrase')}
-
- {showSeedPhrase ? (
-
{
- await onPreferenceToggle(!featureSecondary);
+ await onPreferenceToggle();
setSwapped(!isSwapped);
};
diff --git a/ui/components/app/flask/snap-settings-card/snap-settings-card.js b/ui/components/app/flask/snap-settings-card/snap-settings-card.js
index 95afbc810..1cfde078b 100644
--- a/ui/components/app/flask/snap-settings-card/snap-settings-card.js
+++ b/ui/components/app/flask/snap-settings-card/snap-settings-card.js
@@ -33,10 +33,10 @@ const STATUSES = {
};
const STATUS_COLORS = {
- [STATUSES.INSTALLING]: COLORS.ALERT1,
- [STATUSES.RUNNING]: COLORS.SUCCESS1,
- [STATUSES.STOPPED]: COLORS.UI4,
- [STATUSES.CRASHED]: COLORS.ERROR1,
+ [STATUSES.INSTALLING]: COLORS.WARNING_DEFAULT,
+ [STATUSES.RUNNING]: COLORS.SUCCESS_DEFAULT,
+ [STATUSES.STOPPED]: COLORS.ICON_MUTED,
+ [STATUSES.CRASHED]: COLORS.ERROR_DEFAULT,
};
const SnapSettingsCard = ({
@@ -81,7 +81,7 @@ const SnapSettingsCard = ({
marginTop: 0,
marginBottom: 0,
}}
- color={COLORS.BLACK}
+ color={COLORS.TEXT_DEFAULT}
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
className="snap-settings-card__title"
@@ -101,7 +101,7 @@ const SnapSettingsCard = ({
@@ -158,7 +158,7 @@ const SnapSettingsCard = ({
boxProps={{
margin: [0, 0],
}}
- color={COLORS.UI3}
+ color={COLORS.TEXT_MUTED}
variant={TYPOGRAPHY.H8}
fontWeight={FONT_WEIGHT.NORMAL}
tag="span"
@@ -173,7 +173,7 @@ const SnapSettingsCard = ({
paddingLeft: 2,
margin: [0, 0],
}}
- color={COLORS.UI4}
+ color={COLORS.TEXT_MUTED}
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.NORMAL}
align={TEXT_ALIGN.CENTER}
diff --git a/ui/components/app/flask/snaps-authorship-pill/index.scss b/ui/components/app/flask/snaps-authorship-pill/index.scss
index 6a9b0c15e..a67d876d7 100644
--- a/ui/components/app/flask/snaps-authorship-pill/index.scss
+++ b/ui/components/app/flask/snaps-authorship-pill/index.scss
@@ -6,11 +6,11 @@
&:hover,
&:focus {
.chip {
- background-color: var(--ui-1);
+ background-color: var(--color-background-alternative);
}
}
}
.snaps-authorship-icon {
- color: var(--ui-4);
+ color: var(--color-icon-default);
}
diff --git a/ui/components/app/flask/snaps-authorship-pill/snaps-authorship-pill.js b/ui/components/app/flask/snaps-authorship-pill/snaps-authorship-pill.js
index 584c3c686..6625694b7 100644
--- a/ui/components/app/flask/snaps-authorship-pill/snaps-authorship-pill.js
+++ b/ui/components/app/flask/snaps-authorship-pill/snaps-authorship-pill.js
@@ -23,13 +23,13 @@ const SnapsAuthorshipPill = ({ packageName, className, url }) => {
}
- backgroundColor={COLORS.WHITE}
+ backgroundColor={COLORS.BACKGROUND_DEFAULT}
>
{packageName}
diff --git a/ui/components/app/gas-details-item/gas-details-item.js b/ui/components/app/gas-details-item/gas-details-item.js
index c47b3a1dd..8c3977f19 100644
--- a/ui/components/app/gas-details-item/gas-details-item.js
+++ b/ui/components/app/gas-details-item/gas-details-item.js
@@ -5,7 +5,6 @@ import { useSelector } from 'react-redux';
import { COLORS } from '../../../helpers/constants/design-system';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
-import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util';
import { getPreferences } from '../../../selectors';
import { useGasFeeContext } from '../../../contexts/gasFee';
@@ -23,7 +22,8 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
hasSimulationError,
maximumCostInHexWei: hexMaximumTransactionFee,
minimumCostInHexWei: hexMinimumTransactionFee,
- transaction,
+ maxPriorityFeePerGas,
+ maxFeePerGas,
} = useGasFeeContext();
const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences);
@@ -90,10 +90,8 @@ const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
}
subTitle={
}
/>
diff --git a/ui/components/app/signature-request/signature-request-message/index.scss b/ui/components/app/signature-request/signature-request-message/index.scss
index 14f8d4f39..6673d38f7 100644
--- a/ui/components/app/signature-request/signature-request-message/index.scss
+++ b/ui/components/app/signature-request/signature-request-message/index.scss
@@ -1,7 +1,7 @@
.signature-request-message {
flex: 1 60%;
display: flex;
- max-height: 250px;
+ max-height: 230px;
flex-direction: column;
position: relative;
diff --git a/ui/components/app/srp-input/index.js b/ui/components/app/srp-input/index.js
new file mode 100644
index 000000000..60350cf68
--- /dev/null
+++ b/ui/components/app/srp-input/index.js
@@ -0,0 +1 @@
+export { default } from './srp-input';
diff --git a/ui/components/app/create-new-vault/parse-secret-recovery-phrase'.test.js b/ui/components/app/srp-input/parse-secret-recovery-phrase'.test.js
similarity index 100%
rename from ui/components/app/create-new-vault/parse-secret-recovery-phrase'.test.js
rename to ui/components/app/srp-input/parse-secret-recovery-phrase'.test.js
diff --git a/ui/components/app/create-new-vault/parse-secret-recovery-phrase.js b/ui/components/app/srp-input/parse-secret-recovery-phrase.js
similarity index 100%
rename from ui/components/app/create-new-vault/parse-secret-recovery-phrase.js
rename to ui/components/app/srp-input/parse-secret-recovery-phrase.js
diff --git a/ui/components/app/srp-input/srp-input.js b/ui/components/app/srp-input/srp-input.js
new file mode 100644
index 000000000..2369cfa98
--- /dev/null
+++ b/ui/components/app/srp-input/srp-input.js
@@ -0,0 +1,112 @@
+import { ethers } from 'ethers';
+import React, { useCallback, useState } from 'react';
+import PropTypes from 'prop-types';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import TextField from '../../ui/text-field';
+import { clearClipboard } from '../../../helpers/utils/util';
+import CheckBox from '../../ui/check-box';
+import Typography from '../../ui/typography';
+import { COLORS } from '../../../helpers/constants/design-system';
+import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase';
+
+const { isValidMnemonic } = ethers.utils;
+
+export default function SrpInput({ onChange }) {
+ const [srpError, setSrpError] = useState('');
+ const [draftSrp, setDraftSrp] = useState('');
+ const [showSrp, setShowSrp] = useState(false);
+
+ const t = useI18nContext();
+
+ const onSrpChange = useCallback(
+ (event) => {
+ const rawSrp = event.target.value;
+ let newSrpError = '';
+ const parsedSeedPhrase = parseSecretRecoveryPhrase(rawSrp);
+
+ if (rawSrp) {
+ const wordCount = parsedSeedPhrase.split(/\s/u).length;
+ if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) {
+ newSrpError = t('seedPhraseReq');
+ } else if (!isValidMnemonic(parsedSeedPhrase)) {
+ newSrpError = t('invalidSeedPhrase');
+ }
+ }
+
+ setDraftSrp(rawSrp);
+ setSrpError(newSrpError);
+ onChange(newSrpError ? '' : parsedSeedPhrase);
+ },
+ [setDraftSrp, setSrpError, t, onChange],
+ );
+
+ const toggleShowSrp = useCallback(() => {
+ setShowSrp((currentShowSrp) => !currentShowSrp);
+ }, []);
+
+ return (
+ <>
+
+ {t('secretRecoveryPhrase')}
+
+ {showSrp ? (
+
+ ) : (
+
+ )}
+ {srpError ? (
+
+ {srpError}
+
+ ) : null}
+
+
+
+ {t('showSeedPhrase')}
+
+
+ >
+ );
+}
+
+SrpInput.propTypes = {
+ /**
+ * Event handler for SRP changes.
+ *
+ * This is only called with a valid, well-formated (i.e. exactly one space
+ * between each word) SRP or with an empty string.
+ *
+ * This is called each time the draft SRP is updated. If the draft SRP is
+ * valid, this is called with a well-formatted version of that draft SRP.
+ * Otherwise, this is called with an empty string.
+ */
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/ui/components/app/srp-input/srp-input.scss b/ui/components/app/srp-input/srp-input.scss
new file mode 100644
index 000000000..355811e76
--- /dev/null
+++ b/ui/components/app/srp-input/srp-input.scss
@@ -0,0 +1,24 @@
+.import-srp {
+ &__srp-label {
+ margin-bottom: 8px;
+ }
+
+ &__srp-shown {
+ @include Paragraph;
+
+ padding: 8px 16px;
+ }
+
+ &__srp-error {
+ margin-top: 4px;
+ }
+
+ &__show-srp {
+ margin-top: 16px;
+ margin-bottom: 16px;
+ }
+
+ &__show-srp-label {
+ margin-left: 8px;
+ }
+}
diff --git a/ui/components/app/srp-input/srp-input.stories.js b/ui/components/app/srp-input/srp-input.stories.js
new file mode 100644
index 000000000..2e9eee21d
--- /dev/null
+++ b/ui/components/app/srp-input/srp-input.stories.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import SrpInput from '.';
+
+export default {
+ title: 'Components/App/SrpInput',
+ id: __filename,
+ argTypes: {
+ onChange: { action: 'changed' },
+ },
+ component: SrpInput,
+};
+
+const Template = (args) => {
+ return (
+
+
+
+ );
+};
+
+export const DefaultStory = Template.bind({});
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/srp-input/srp-input.test.js b/ui/components/app/srp-input/srp-input.test.js
new file mode 100644
index 000000000..af024119c
--- /dev/null
+++ b/ui/components/app/srp-input/srp-input.test.js
@@ -0,0 +1,543 @@
+import React from 'react';
+import { waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import enLocale from '../../../../app/_locales/en/messages.json';
+import { renderWithLocalization } from '../../../../test/lib/render-helpers';
+import SrpInput from '.';
+
+const tooFewWords = new Array(11).fill('test').join(' ');
+const tooManyWords = new Array(25).fill('test').join(' ');
+const invalidWordCount = new Array(13).fill('test').join(' ');
+const invalidChecksum = new Array(12).fill('test').join(' ');
+const invalidWordCorrectChecksum = `aardvark ${new Array(10)
+ .fill('test')
+ .join(' ')} wolf`;
+const correct = `${new Array(11).fill('test').join(' ')} ball`;
+
+const invalidInputs = [
+ ' ',
+ 'foo',
+ '🙂',
+ tooFewWords,
+ tooManyWords,
+ invalidWordCount,
+ invalidChecksum,
+ invalidWordCorrectChecksum,
+];
+
+const poorlyFormattedInputs = [
+ ` ${correct}`,
+ `\t${correct}`,
+ `\n${correct}`,
+ `${correct} `,
+ `${correct}\t`,
+ `${correct}\n`,
+ `${new Array(11).fill('test').join(' ')} ball`,
+ `${new Array(11).fill('test').join('\t')}\tball`,
+];
+
+describe('srp-input', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('onChange event', () => {
+ it('should not fire event on render', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ await waitFor(() => getByLabelText(enLocale.showSeedPhrase.message));
+
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ describe('invalid typed inputs', () => {
+ for (const invalidInput of invalidInputs) {
+ it(`should fire event with empty string upon invalid input: '${invalidInput}'`, async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(invalidInput);
+
+ expect(onChange).toHaveBeenLastCalledWith('');
+ });
+ }
+ });
+
+ describe('invalid pasted inputs', () => {
+ for (const invalidInput of invalidInputs) {
+ it(`should fire event with empty string upon invalid pasted input: '${invalidInput}'`, async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(invalidInput);
+
+ expect(onChange).toHaveBeenLastCalledWith('');
+ });
+ }
+ });
+
+ describe('valid typed inputs', () => {
+ it('should fire event with a valid SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+
+ expect(onChange).toHaveBeenLastCalledWith(correct);
+ });
+
+ for (const poorlyFormattedInput of poorlyFormattedInputs) {
+ it(`should fire with formatted SRP when given poorly formatted valid SRP: '${poorlyFormattedInput}'`, async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(poorlyFormattedInput);
+
+ expect(onChange).toHaveBeenLastCalledWith(correct);
+ });
+ }
+ });
+
+ describe('valid pasted inputs', () => {
+ it('should fire event with a valid SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+
+ expect(onChange).toHaveBeenLastCalledWith(correct);
+ });
+
+ for (const poorlyFormattedInput of poorlyFormattedInputs) {
+ it(`should fire with formatted SRP when given poorly formatted valid SRP: '${poorlyFormattedInput}'`, async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(poorlyFormattedInput);
+
+ expect(onChange).toHaveBeenLastCalledWith(correct);
+ });
+ }
+ });
+ });
+
+ describe('error message', () => {
+ it('should not show error for empty input', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ await waitFor(() => getByLabelText(enLocale.showSeedPhrase.message));
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ describe('typed', () => {
+ it('should show word requirement error if SRP has too few words', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(tooFewWords);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ it('should show word requirement error if SRP has too many words', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(tooManyWords);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ it('should show word requirement error if SRP has an unsupported word count above 12 but below 24', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(invalidWordCount);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ it('should show invalid SRP error if SRP is correct length but has an invalid checksum', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(invalidChecksum);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
+ });
+
+ it('should show invalid SRP error if SRP is correct length and has correct checksum but has an invalid word', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(invalidWordCorrectChecksum);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
+ });
+
+ it('should not show error for valid SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ for (const poorlyFormattedInput of poorlyFormattedInputs) {
+ it(`should not show error for poorly formatted valid SRP: '${poorlyFormattedInput}'`, async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(poorlyFormattedInput);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+ }
+ });
+
+ describe('pasted', () => {
+ it('should show word requirement error if SRP has too few words', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(tooFewWords);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ it('should show word requirement error if SRP has too many words', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(tooManyWords);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ it('should show word requirement error if SRP has an unsupported word count above 12 but below 24', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(invalidWordCount);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ it('should show invalid SRP error if SRP is correct length but has an invalid checksum', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(invalidChecksum);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
+ });
+
+ it('should show invalid SRP error if SRP is correct length and has correct checksum but has an invalid word', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(invalidWordCorrectChecksum);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
+ });
+
+ it('should not show error for valid SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+
+ for (const poorlyFormattedInput of poorlyFormattedInputs) {
+ it(`should not show error for poorly formatted valid SRP: '${poorlyFormattedInput}'`, async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(poorlyFormattedInput);
+
+ expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
+ expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
+ });
+ }
+ });
+ });
+
+ describe('Show/hide SRP', () => {
+ it('should default to not showing SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, getByRole } = renderWithLocalization(
+ ,
+ );
+
+ expect(
+ getByLabelText(enLocale.secretRecoveryPhrase.message),
+ ).toHaveAttribute('type', 'password');
+ expect(getByRole('checkbox')).not.toBeChecked();
+ });
+
+ describe('default hidden', () => {
+ it('should prevent reading typed SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+
+ expect(queryByText(correct)).toBeNull();
+ });
+
+ it('should prevent reading pasted SRP', async () => {
+ const onChange = jest.fn();
+
+ const { getByLabelText, queryByText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+
+ expect(queryByText(correct)).toBeNull();
+ });
+ });
+
+ describe('shown then hidden', () => {
+ it('should prevent reading typed SRP', async () => {
+ const onChange = jest.fn();
+
+ const {
+ getByLabelText,
+ getByRole,
+ queryByText,
+ } = renderWithLocalization( );
+ await userEvent.click(getByRole('checkbox'));
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+ await userEvent.click(getByRole('checkbox'));
+
+ expect(queryByText(correct)).toBeNull();
+ });
+
+ it('should prevent reading pasted SRP', async () => {
+ const onChange = jest.fn();
+
+ const {
+ getByLabelText,
+ getByRole,
+ queryByText,
+ } = renderWithLocalization( );
+ await userEvent.click(getByRole('checkbox'));
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+ await userEvent.click(getByRole('checkbox'));
+
+ expect(queryByText(correct)).toBeNull();
+ });
+ });
+
+ describe('shown after input', () => {
+ it('should show typed SRP', async () => {
+ const onChange = jest.fn();
+
+ const {
+ getByLabelText,
+ getByRole,
+ queryByText,
+ } = renderWithLocalization( );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+ await userEvent.click(getByRole('checkbox'));
+
+ expect(queryByText(correct)).not.toBeNull();
+ });
+
+ it('should show pasted SRP', async () => {
+ const onChange = jest.fn();
+
+ const {
+ getByLabelText,
+ getByRole,
+ queryByText,
+ } = renderWithLocalization( );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+ await userEvent.click(getByRole('checkbox'));
+
+ expect(queryByText(correct)).not.toBeNull();
+ });
+ });
+
+ describe('shown before input', () => {
+ it('should show typed SRP', async () => {
+ const onChange = jest.fn();
+
+ const {
+ getByLabelText,
+ getByRole,
+ queryByText,
+ } = renderWithLocalization( );
+ await userEvent.click(getByRole('checkbox'));
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+
+ expect(queryByText(correct)).not.toBeNull();
+ });
+
+ it('should show pasted SRP', async () => {
+ const onChange = jest.fn();
+
+ const {
+ getByLabelText,
+ getByRole,
+ queryByText,
+ } = renderWithLocalization( );
+ await userEvent.click(getByRole('checkbox'));
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+
+ expect(queryByText(correct)).not.toBeNull();
+ });
+ });
+ });
+
+ describe('clear after paste', () => {
+ it('should not clear clipboard after typing hidden SRP', async () => {
+ const onChange = jest.fn();
+ const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+
+ expect(writeTextSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not clear clipboard after typing shown SRP', async () => {
+ const onChange = jest.fn();
+ const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
+
+ const { getByLabelText, getByRole } = renderWithLocalization(
+ ,
+ );
+ await userEvent.click(getByRole('checkbox'));
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.keyboard(correct);
+
+ expect(writeTextSpy).not.toHaveBeenCalled();
+ });
+
+ it('should clear the clipboard after pasting hidden SRP', async () => {
+ const onChange = jest.fn();
+ const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
+
+ const { getByLabelText } = renderWithLocalization(
+ ,
+ );
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+
+ expect(writeTextSpy).toHaveBeenCalledWith('');
+ });
+
+ it('should clear the clipboard after pasting shown SRP', async () => {
+ const onChange = jest.fn();
+ const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
+
+ const { getByLabelText, getByRole } = renderWithLocalization(
+ ,
+ );
+ await userEvent.click(getByRole('checkbox'));
+ getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
+ await userEvent.paste(correct);
+
+ expect(writeTextSpy).toHaveBeenCalledWith('');
+ });
+ });
+});
diff --git a/ui/components/app/tab-bar/index.scss b/ui/components/app/tab-bar/index.scss
index 87b898c79..17ce585b7 100644
--- a/ui/components/app/tab-bar/index.scss
+++ b/ui/components/app/tab-bar/index.scss
@@ -41,7 +41,6 @@
flex-flow: row wrap;
align-items: center;
position: relative;
- z-index: 201;
&__title {
@include H4;
diff --git a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js
index 7ccd78642..042b73c24 100644
--- a/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js
+++ b/ui/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js
@@ -14,7 +14,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
- onPreferenceToggle: (value) => dispatch(toggleCurrencySwitch(value)),
+ onPreferenceToggle: () => dispatch(toggleCurrencySwitch()),
};
};
diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js
index 1ecfe4719..001f84694 100644
--- a/ui/components/app/wallet-overview/eth-overview.js
+++ b/ui/components/app/wallet-overview/eth-overview.js
@@ -55,7 +55,7 @@ const EthOverview = ({ className }) => {
});
const history = useHistory();
const keyring = useSelector(getCurrentKeyring);
- const usingHardwareWallet = isHardwareKeyring(keyring.type);
+ const usingHardwareWallet = isHardwareKeyring(keyring?.type);
const balanceIsCached = useSelector(isBalanceCached);
const showFiat = useSelector(getShouldShowFiat);
const selectedAccount = useSelector(getSelectedAccount);
diff --git a/ui/components/ui/account-list/index.scss b/ui/components/ui/account-list/index.scss
index a523c5341..712425f8e 100644
--- a/ui/components/ui/account-list/index.scss
+++ b/ui/components/ui/account-list/index.scss
@@ -35,11 +35,12 @@
&__wrapper {
width: 92%;
display: flex;
+ overflow: hidden;
}
&__list {
flex: 2 1 0;
- width: 92%;
+ width: 100%;
max-height: max-content;
border: 1px solid #d0d5da;
box-sizing: border-box;
diff --git a/ui/components/ui/box/README.mdx b/ui/components/ui/box/README.mdx
new file mode 100644
index 000000000..71f7dd42a
--- /dev/null
+++ b/ui/components/ui/box/README.mdx
@@ -0,0 +1,124 @@
+import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
+
+import ActionableMessage from '../actionable-message';
+
+import Box from '.';
+
+# Box
+
+Box is a utility component that can be used for layout or as a base for other UI components.
+
+
+
+
+
+## Component API
+
+| Name | Description | Default |
+| --------------- | ----------------------------------- | ------------------ |
+| children | node func | - |
+| flexDirection | Object.values(FLEX_DIRECTION) | FLEX_DIRECTION.ROW |
+| flexWrap | Object.values(FLEX_WRAP) | - |
+| gap | 1,2,4,6,8 | - |
+| margin | 1,2,4,6,8 or array of numbers [1,2] | - |
+| marginTop | 1,2,4,6,8 | - |
+| marginBottom | 1,2,4,6,8 | - |
+| marginRight | 1,2,4,6,8 | - |
+| marginLeft | 1,2,4,6,8 | - |
+| padding | 1,2,4,6,8 or array of numbers [1,2] | - |
+| paddingTop | 1,2,4,6,8 | - |
+| paddingBottom | 1,2,4,6,8 | - |
+| paddingRight | 1,2,4,6,8 | - |
+| paddingLeft | 1,2,4,6,8 | - |
+| borderColor | Object.values(COLORS) | - |
+| borderWidth | number | - |
+| borderRadius | Object.values(SIZES) | - |
+| borderStyle | Object.values(BORDER_STYLE) | - |
+| alignItems | Object.values(ALIGN_ITEMS) | - |
+| justifyContent | Object.values(JUSTIFY_CONTENT) | - |
+| textAlign | Object.values(TEXT_ALIGN) | - |
+| display | Object.values(DISPLAY) | - |
+| width | Object.values(BLOCK_SIZES) | - |
+| height | Object.values(BLOCK_SIZES) | - |
+| backgroundColor | Object.values(COLORS) | - |
+| className | string | |
+
+## Usage
+
+The following describes the props and example usage for this component.
+
+### Background Color
+
+Use the `backgroundColor` prop along with the `COLORS` object from `ui/helpers/constants/design-system.js` to change background color.
+
+
+
+
+
+**NOTE**: The ` ` and ` ` color combinations above follow our design system color rules and should cover most general UI applications. Click "Show code" to see the code example. Do not use the [deprecated colors](#deprecated-colors)
+
+Example of importing `COLORS` object with `Box` component
+
+```jsx
+import { COLORS } from '../../../helpers/constants/design-system';
+import Box from '../ui/box';
+
+
+ COLORS.BACKGROUND_DEFAULT
+ ;
+```
+
+### Border Color
+
+Use the `borderColor` prop along with the `COLORS` object from `ui/helpers/constants/design-system.js` to change border color
+
+
+
+
+
+**NOTE**: The ` ` and ` ` color combinations above follow our design system color rules and should cover most general UI applications. Click "Show code" to see the code example. Do not use the [deprecated colors](#deprecated-colors)
+
+Example of importing `COLORS` object with `Box` component
+
+```jsx
+import { COLORS } from '../../../helpers/constants/design-system';
+import Box from '../ui/box';
+
+
+ COLORS.BORDER_DEFAULT
+ ;
+```
+
+## Deprecated Colors
+
+List of deprecated background and border color props that are not theme compatible and should not be used.
+
+```js
+/** !!! DEPRECATED DO NOT USE!!! */
+UI1: 'ui-1',
+UI2: 'ui-2',
+UI3: 'ui-3',
+UI4: 'ui-4',
+BLACK: 'black',
+GREY: 'grey',
+NEUTRAL_GREY: 'neutral-grey',
+WHITE: 'white',
+PRIMARY1: 'primary-1',
+PRIMARY2: 'primary-2',
+PRIMARY3: 'primary-3',
+SECONDARY1: 'secondary-1',
+SECONDARY2: 'secondary-2',
+SECONDARY3: 'secondary-3',
+SUCCESS1: 'success-1',
+SUCCESS2: 'success-2',
+SUCCESS3: 'success-3',
+ERROR1: 'error-1',
+ERROR2: 'error-2',
+ERROR3: 'error-3',
+ALERT1: 'alert-1',
+ALERT2: 'alert-2',
+ALERT3: 'alert-3',
+```
diff --git a/ui/components/ui/box/box.js b/ui/components/ui/box/box.js
index 91d0e8817..73dbb37fe 100644
--- a/ui/components/ui/box/box.js
+++ b/ui/components/ui/box/box.js
@@ -30,6 +30,80 @@ const ValidSize = PropTypes.oneOf([
12,
'auto',
]);
+
+export const ValidBackgroundColors = [
+ COLORS.BACKGROUND_DEFAULT,
+ COLORS.BACKGROUND_ALTERNATIVE,
+ COLORS.OVERLAY_DEFAULT,
+ COLORS.PRIMARY_DEFAULT,
+ COLORS.PRIMARY_ALTERNATIVE,
+ COLORS.PRIMARY_MUTED,
+ COLORS.PRIMARY_DISABLED,
+ COLORS.SECONDARY_DEFAULT,
+ COLORS.SECONDARY_ALTERNATIVE,
+ COLORS.SECONDARY_MUTED,
+ COLORS.SECONDARY_DISABLED,
+ COLORS.ERROR_DEFAULT,
+ COLORS.ERROR_ALTERNATIVE,
+ COLORS.ERROR_MUTED,
+ COLORS.ERROR_DISABLED,
+ COLORS.WARNING_DEFAULT,
+ COLORS.WARNING_ALTERNATIVE,
+ COLORS.WARNING_MUTED,
+ COLORS.WARNING_DISABLED,
+ COLORS.SUCCESS_DEFAULT,
+ COLORS.SUCCESS_ALTERNATIVE,
+ COLORS.SUCCESS_MUTED,
+ COLORS.SUCCESS_DISABLED,
+ COLORS.INFO_DEFAULT,
+ COLORS.INFO_ALTERNATIVE,
+ COLORS.INFO_MUTED,
+ COLORS.INFO_DISABLED,
+ COLORS.MAINNET,
+ COLORS.ROPSTEN,
+ COLORS.KOVAN,
+ COLORS.RINKEBY,
+ COLORS.GOERLI,
+ COLORS.TRANSPARENT,
+ COLORS.LOCALHOST,
+];
+
+export const ValidBorderColors = [
+ COLORS.BORDER_DEFAULT,
+ COLORS.BORDER_MUTED,
+ COLORS.PRIMARY_DEFAULT,
+ COLORS.PRIMARY_ALTERNATIVE,
+ COLORS.PRIMARY_MUTED,
+ COLORS.PRIMARY_DISABLED,
+ COLORS.SECONDARY_DEFAULT,
+ COLORS.SECONDARY_ALTERNATIVE,
+ COLORS.SECONDARY_MUTED,
+ COLORS.SECONDARY_DISABLED,
+ COLORS.ERROR_DEFAULT,
+ COLORS.ERROR_ALTERNATIVE,
+ COLORS.ERROR_MUTED,
+ COLORS.ERROR_DISABLED,
+ COLORS.WARNING_DEFAULT,
+ COLORS.WARNING_ALTERNATIVE,
+ COLORS.WARNING_MUTED,
+ COLORS.WARNING_DISABLED,
+ COLORS.SUCCESS_DEFAULT,
+ COLORS.SUCCESS_ALTERNATIVE,
+ COLORS.SUCCESS_MUTED,
+ COLORS.SUCCESS_DISABLED,
+ COLORS.INFO_DEFAULT,
+ COLORS.INFO_ALTERNATIVE,
+ COLORS.INFO_MUTED,
+ COLORS.INFO_DISABLED,
+ COLORS.MAINNET,
+ COLORS.ROPSTEN,
+ COLORS.KOVAN,
+ COLORS.RINKEBY,
+ COLORS.GOERLI,
+ COLORS.TRANSPARENT,
+ COLORS.LOCALHOST,
+];
+
const ArrayOfValidSizes = PropTypes.arrayOf(ValidSize);
export const MultipleSizes = PropTypes.oneOfType([
ValidSize,
@@ -180,7 +254,7 @@ Box.propTypes = {
paddingBottom: ValidSize,
paddingRight: ValidSize,
paddingLeft: ValidSize,
- borderColor: PropTypes.oneOf(Object.values(COLORS)),
+ borderColor: PropTypes.oneOf(Object.values(ValidBorderColors)),
borderWidth: PropTypes.number,
borderRadius: PropTypes.oneOf(Object.values(SIZES)),
borderStyle: PropTypes.oneOf(Object.values(BORDER_STYLE)),
@@ -190,6 +264,6 @@ Box.propTypes = {
display: PropTypes.oneOf(Object.values(DISPLAY)),
width: PropTypes.oneOf(Object.values(BLOCK_SIZES)),
height: PropTypes.oneOf(Object.values(BLOCK_SIZES)),
- backgroundColor: PropTypes.oneOf(Object.values(COLORS)),
+ backgroundColor: PropTypes.oneOf(Object.values(ValidBackgroundColors)),
className: PropTypes.string,
};
diff --git a/ui/components/ui/box/box.stories.js b/ui/components/ui/box/box.stories.js
index 382927a78..7044e48e7 100644
--- a/ui/components/ui/box/box.stories.js
+++ b/ui/components/ui/box/box.stories.js
@@ -8,8 +8,14 @@ import {
JUSTIFY_CONTENT,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
+
+import Typography from '../typography';
+
import Box from './box';
+import README from './README.mdx';
+import { ValidBackgroundColors, ValidBorderColors } from '.';
+
const sizeKnobOptions = [undefined, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const marginSizeKnobOptions = [...sizeKnobOptions, 'auto'];
@@ -17,6 +23,11 @@ export default {
title: 'Components/UI/Box',
id: __filename,
component: Box,
+ parameters: {
+ docs: {
+ page: README,
+ },
+ },
argTypes: {
size: {
control: { type: 'range', min: 50, max: 500, step: 10 },
@@ -29,37 +40,50 @@ export default {
defaultValue: 1,
},
display: {
- options: DISPLAY,
+ options: Object.values(DISPLAY),
control: 'select',
defaultValue: DISPLAY.BLOCK,
table: { category: 'display' },
},
width: {
- options: BLOCK_SIZES,
+ options: Object.values(BLOCK_SIZES),
control: 'select',
defaultValue: BLOCK_SIZES.HALF,
table: { category: 'display' },
},
height: {
- options: BLOCK_SIZES,
+ options: Object.values(BLOCK_SIZES),
control: 'select',
defaultValue: BLOCK_SIZES.HALF,
table: { category: 'display' },
},
+ backgroundColor: {
+ options: ValidBackgroundColors,
+ control: 'select',
+ table: {
+ category: 'background',
+ },
+ },
+ borderColor: {
+ options: ValidBorderColors,
+ control: 'select',
+ defaultValue: COLORS.BORDER_DEFAULT,
+ table: { category: 'border' },
+ },
justifyContent: {
- options: JUSTIFY_CONTENT,
+ options: Object.values(JUSTIFY_CONTENT),
control: 'select',
defaultValue: JUSTIFY_CONTENT.FLEX_START,
table: { category: 'display' },
},
alignItems: {
- options: ALIGN_ITEMS,
+ options: Object.values(ALIGN_ITEMS),
control: 'select',
defaultValue: ALIGN_ITEMS.FLEX_START,
table: { category: 'display' },
},
textAlign: {
- options: TEXT_ALIGN,
+ options: Object.values(TEXT_ALIGN),
control: 'select',
defaultValue: TEXT_ALIGN.LEFT,
table: { category: 'left' },
@@ -115,7 +139,7 @@ export default {
table: { category: 'padding' },
},
borderStyle: {
- options: BORDER_STYLE,
+ options: Object.values(BORDER_STYLE),
control: 'select',
defaultValue: BORDER_STYLE.DASHED,
table: { category: 'border' },
@@ -126,18 +150,6 @@ export default {
defaultValue: 1,
table: { category: 'border' },
},
- borderColor: {
- options: COLORS,
- control: 'select',
- defaultValue: COLORS.BLACK,
- table: { category: 'border' },
- },
- backgroundColor: {
- options: COLORS,
- defaultValue: COLORS.WHITE,
- control: 'select',
- table: { category: 'background' },
- },
},
};
@@ -153,3 +165,141 @@ export const DefaultStory = (args) => {
};
DefaultStory.storyName = 'Default';
+
+export const BackgroundColor = () => {
+ return (
+ <>
+
+
+ COLORS.BACKGROUND_DEFAULT
+
+
+
+
+ COLORS.BACKGROUND_ALTERNATIVE
+
+
+
+
+ COLORS.OVERLAY_DEFAULT
+
+
+
+
+ COLORS.PRIMARY_DEFAULT
+
+
+
+
+ COLORS.PRIMARY_MUTED
+
+
+
+
+ COLORS.SECONDARY_DEFAULT
+
+
+
+
+ COLORS.SECONDARY_MUTED
+
+
+
+
+ COLORS.ERROR_DEFAULT
+
+
+
+ COLORS.ERROR_MUTED
+
+
+
+ COLORS.SUCCESS_DEFAULT
+
+
+
+
+ COLORS.SUCCESS_MUTED
+
+
+
+
+ COLORS.WARNING_DEFAULT
+
+
+
+
+ COLORS.WARNING_MUTED
+
+
+ >
+ );
+};
+
+export const BorderColor = () => {
+ return (
+ <>
+
+
+ COLORS.BORDER_DEFAULT
+
+
+
+ COLORS.BORDER_MUTED
+
+
+
+ COLORS.PRIMARY_DEFAULT
+
+
+
+
+ COLORS.SECONDARY_DEFAULT
+
+
+
+
+ COLORS.ERROR_DEFAULT
+
+
+
+
+ COLORS.SUCCESS_DEFAULT
+
+
+
+
+ COLORS.WARNING_DEFAULT
+
+
+ >
+ );
+};
diff --git a/ui/components/ui/box/index.js b/ui/components/ui/box/index.js
index c95505f0b..e599a9a98 100644
--- a/ui/components/ui/box/index.js
+++ b/ui/components/ui/box/index.js
@@ -1 +1,6 @@
-export { default, MultipleSizes } from './box';
+export {
+ default,
+ MultipleSizes,
+ ValidBackgroundColors,
+ ValidBorderColors,
+} from './box';
diff --git a/ui/components/ui/button/button.component.js b/ui/components/ui/button/button.component.js
index a265f6294..b43bafda1 100644
--- a/ui/components/ui/button/button.component.js
+++ b/ui/components/ui/button/button.component.js
@@ -5,11 +5,9 @@ import classnames from 'classnames';
const CLASSNAME_DEFAULT = 'btn-default';
const CLASSNAME_PRIMARY = 'btn-primary';
const CLASSNAME_SECONDARY = 'btn-secondary';
-const CLASSNAME_CONFIRM = 'btn-primary';
const CLASSNAME_RAISED = 'btn-raised';
const CLASSNAME_LARGE = 'btn--large';
const CLASSNAME_ROUNDED = 'btn--rounded';
-const CLASSNAME_FIRST_TIME = 'btn--first-time';
const CLASSNAME_INLINE = 'btn--inline';
const typeHash = {
@@ -21,10 +19,7 @@ const typeHash = {
'danger-primary': 'btn-danger-primary',
link: 'btn-link',
inline: CLASSNAME_INLINE,
- // TODO: Legacy button type to be deprecated
- confirm: CLASSNAME_CONFIRM,
raised: CLASSNAME_RAISED,
- 'first-time': CLASSNAME_FIRST_TIME,
};
const Button = ({
diff --git a/ui/components/ui/button/button.stories.js b/ui/components/ui/button/button.stories.js
index 3512bb4ba..8ce47a549 100644
--- a/ui/components/ui/button/button.stories.js
+++ b/ui/components/ui/button/button.stories.js
@@ -70,24 +70,35 @@ export const Type = (args) => (
{args.children || 'Default'}
+
{args.children || 'Primary'}
+
{args.children || 'Secondary'}
+
{args.children || 'Warning'}
+
{args.children || 'Danger'}
+
{args.children || 'Danger primary'}
+
+
+ {args.children || 'Raised'}
+
+
{args.children || 'Link'}
+
{args.children || 'Inline'}
diff --git a/ui/components/ui/button/buttons.scss b/ui/components/ui/button/buttons.scss
index c0f61531f..81f18d4a2 100644
--- a/ui/components/ui/button/buttons.scss
+++ b/ui/components/ui/button/buttons.scss
@@ -3,14 +3,6 @@
*/
.button {
- --hover-secondary: #b0d7f2;
- --hover-default: #b3b3b3;
- --hover-confirm: #0372c3;
- --hover-red: #feb6bf;
- --hover-red-primary: #c72837;
- --hover-orange: #ffd3b5;
- --warning-light-orange: #f8b588;
-
@include H6;
font-weight: 500;
@@ -38,146 +30,148 @@
}
.btn-secondary {
- color: var(--Blue-500);
- border: 1px solid var(--hover-secondary);
- background-color: var(--white);
+ color: var(--color-primary-default);
+ border: 1px solid var(--color-primary-muted);
+ background-color: var(--color-background-default);
&:hover {
- border-color: var(--Blue-500);
+ border-color: var(--color-primary-default);
}
&:active {
- background: var(--Blue-000);
- border-color: var(--Blue-500);
+ background: var(--color-primary-muted);
+ border-color: var(--color-primary-default);
}
&--disabled,
&[disabled] {
opacity: 1;
- color: var(--hover-secondary);
+ color: var(--color-primary-muted);
}
}
.btn-warning {
- color: var(--Orange-500);
- border: 1px solid var(--hover-orange);
- background-color: var(--white);
+ color: var(--color-text-default);
+ border: 1px solid var(--color-warning-default);
+ background-color: var(--color-background-default);
&:hover {
- border-color: var(--Orange-500);
+ border: 1px solid var(--color-warning-default);
}
&:active {
- background: var(--Orange-000);
- border-color: var(--Orange-500);
+ background: var(--color-warning-muted);
+ border: 1px solid var(--color-warning-alternative);
}
&--disabled,
&[disabled] {
opacity: 1;
- color: var(--hover-orange);
+ color: var(--color-text-muted);
}
}
.btn-danger {
- color: var(--Red-500);
- border: 1px solid var(--hover-red);
- background-color: var(--white);
+ color: var(--color-error-default);
+ border: 1px solid var(--color-error-muted);
+ background-color: var(--color-background-default);
&:hover {
- border-color: var(--Red-500);
+ border-color: var(--color-error-default);
}
&:active {
- background: var(--Red-000);
- border-color: var(--Red-500);
+ background: var(--color-error-muted);
+ border-color: var(--color-error-default);
}
&--disabled,
&[disabled] {
opacity: 1;
- color: var(--hover-red);
+ color: var(--color-error-disabled);
}
}
.btn-danger-primary {
- color: var(--white);
- border: 1px solid var(--Red-500);
- background-color: var(--Red-500);
+ color: var(--color-error-inverse);
+ border: 1px solid;
+ border-color: var(--color-error-default);
+ background-color: var(--color-error-default);
&:hover {
- border-color: var(--hover-red-primary);
- background-color: var(--hover-red-primary);
+ border-color: var(--color-error-alternative);
+ background-color: var(--color-error-alternative);
}
&:active {
- background: var(--Red-600);
- border-color: var(--Red-600);
+ background: var(--color-error-alternative0);
+ border-color: var(--color-error-alternative);
}
&--disabled,
&[disabled] {
opacity: 1;
- border-color: var(--hover-red);
- background-color: var(--hover-red);
+ border-color: var(--color-error-disabled);
+ background-color: var(--color-error-disabled);
}
}
.btn-default {
- color: var(--Grey-500);
- border: 1px solid var(--hover-default);
+ color: var(--color-text-alternative);
+ border: 1px solid var(--color-border-default);
+ background: var(--color-background-default);
&:hover {
- border-color: var(--Grey-500);
+ border-color: var(--color-border-default);
}
&:active {
- background: #fbfbfc;
- border-color: var(--Grey-500);
+ background: var(--color-background-alternative);
+ border-color: var(--color-border-default);
}
&--disabled,
&[disabled] {
opacity: 1;
- color: var(--hover-default);
+ color: var(--color-text-muted);
}
}
.btn-primary {
- color: var(--white);
- border: 1px solid var(--Blue-500);
- background-color: var(--Blue-500);
+ color: var(--color-background-default);
+ border: 1px solid var(--color-primary-default);
+ background-color: var(--color-primary-default);
&:hover {
- border-color: var(--hover-confirm);
- background-color: var(--hover-confirm);
+ border-color: var(--color-primary-alternative);
+ background-color: var(--color-primary-alternative);
}
&:active {
- background: var(--Blue-600);
- border-color: var(--Blue-600);
+ background: var(--color-primary-alternative);
+ border-color: var(--color-primary-alternative);
}
&--disabled,
&[disabled] {
- border-color: var(--hover-secondary);
- background-color: var(--hover-secondary);
+ border-color: var(--color-primary-disabled);
+ background-color: var(--color-primary-disabled);
}
}
.btn-link {
@include H4;
- color: var(--Blue-500);
+ color: var(--color-primary-default);
cursor: pointer;
background-color: transparent;
&:hover {
- color: var(--Blue-400);
+ color: var(--color-primary-alternative);
}
&:active {
- color: var(--Blue-600);
+ color: var(--color-primary-alternative);
}
&--disabled,
@@ -185,7 +179,7 @@
cursor: auto;
opacity: 1;
pointer-events: none;
- color: var(--hover-secondary);
+ color: var(--color-primary-disabled);
}
}
@@ -198,8 +192,8 @@
*/
.btn-raised {
- color: var(--primary-blue);
- background-color: var(--white);
+ color: var(--color-primary-default);
+ background-color: var(--color-background-default);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
padding: 6px;
height: initial;
@@ -208,19 +202,6 @@
min-width: initial;
}
-.btn--first-time {
- @include H4;
-
- height: 54px;
- width: 198px;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.14);
- color: var(--white);
- font-weight: 500;
- transition: 200ms ease-in-out;
- background-color: rgba(247, 134, 28, 0.9);
- border-radius: 0;
-}
-
button[disabled],
input[type="submit"][disabled] {
cursor: not-allowed;
@@ -237,84 +218,84 @@ input[type="submit"][disabled] {
}
&.btn-secondary {
- border: 1px solid var(--Blue-500);
+ border: 1px solid var(--color-primary-default);
&--disabled,
&[disabled] {
- border-color: var(--hover-secondary);
- color: var(--hover-secondary);
+ border-color: var(--color-primary-disabled);
+ color: var(--color-primary-disabled);
}
&:active {
- border-color: var(--Blue-600);
+ border-color: var(--color-primary-alternative);
}
}
&.btn-default {
- border: 1px solid var(--Grey-500);
+ border: 1px solid var(--color-icon-default);
&--disabled,
&[disabled] {
- border-color: var(--Grey-100);
- color: var(--hover-default);
+ border-color: var(--color-border-muted);
+ color: var(--color-text-muted);
}
&:active {
- border-color: var(--Grey-600);
+ border-color: var(--color-text-alternative);
}
}
&.btn-danger {
- border: 1px solid var(--Red-500);
+ border: 1px solid var(--color-error-default);
&--disabled,
&[disabled] {
- border-color: var(--Red-100);
- color: var(--Red-300);
+ border-color: var(--color-error-disabled);
+ color: var(--color-error-disabled);
}
&:active {
- border-color: var(--Red-600);
+ border-color: var(--color-error-alternative);
}
}
&.btn-warning {
- border: 1px solid var(--Orange-500);
+ border: 1px solid var(--color-warning-default);
&--disabled,
&[disabled] {
- border-color: var(--warning-light-orange);
- color: var(--warning-light-orange);
+ border-color: var(--color-warning-alternative);
+ color: var(--color-text-muted);
}
&:active {
- border-color: var(--Orange-600);
+ border-color: var(--color-warning-alternative);
}
}
&.btn-primary {
- background-color: var(--Blue-500);
+ background-color: var(--color-primary-default);
&--disabled,
&[disabled] {
- background-color: var(--hover-secondary);
+ background-color: var(--color-primary-disabled);
}
&:active {
- background-color: var(--Blue-600);
+ background-color: var(--color-primary-alternative);
}
}
&.btn-danger-primary {
- background-color: var(--Red-500);
+ background-color: var(--color-error-default);
&--disabled,
&[disabled] {
- background-color: var(--Red-300);
+ background-color: var(--color-error-disabled);
}
&:active {
- background-color: var(--Red-600);
+ background-color: var(--color-error-alternative);
}
}
}
@@ -324,16 +305,16 @@ input[type="submit"][disabled] {
padding: 0 4px;
font-size: inherit;
width: auto;
- color: var(--Blue-500);
+ color: var(--color-primary-default);
cursor: pointer;
background-color: transparent;
&:hover {
- color: var(--Blue-400);
+ color: var(--color-primary-alternative);
}
&:active {
- color: var(--Blue-600);
+ color: var(--color-primary-alternative);
}
&--disabled,
@@ -341,6 +322,6 @@ input[type="submit"][disabled] {
cursor: auto;
opacity: 1;
pointer-events: none;
- color: var(--hover-secondary);
+ color: var(--color-primary-disabled);
}
}
diff --git a/ui/components/ui/callout/callout.js b/ui/components/ui/callout/callout.js
index bba3efdf6..a04a4d30b 100644
--- a/ui/components/ui/callout/callout.js
+++ b/ui/components/ui/callout/callout.js
@@ -2,8 +2,9 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import InfoIconInverted from '../icon/info-icon-inverted.component';
-import { SEVERITIES } from '../../../helpers/constants/design-system';
+import { SEVERITIES, COLORS } from '../../../helpers/constants/design-system';
import { MILLISECOND } from '../../../../shared/constants/time';
+import Typography from '../typography';
export default function Callout({
severity,
@@ -36,7 +37,9 @@ export default function Callout({
return (
-
{children}
+
+ {children}
+
{dismiss && (
{
diff --git a/ui/components/ui/callout/callout.scss b/ui/components/ui/callout/callout.scss
index c73ed31ad..ae19d8304 100644
--- a/ui/components/ui/callout/callout.scss
+++ b/ui/components/ui/callout/callout.scss
@@ -10,12 +10,12 @@
transition: opacity 0.75s 0s;
a {
- color: var(--primary-1);
+ color: var(--color-primary-default);
}
&--dismissible {
{$self}--first {
- box-shadow: 0 -5px 5px -5px rgba(0, 0, 0, 0.18);
+ box-shadow: 0 -5px 5px -5px var(--color-overlay-default);
}
}
@@ -37,19 +37,19 @@
}
&--warning {
- border-left: 2px solid var(--alert-1);
+ border-left: 2px solid var(--color-warning-default);
}
&--danger {
- border-left: 2px solid var(--error-1);
+ border-left: 2px solid var(--color-error-default);
}
&--info {
- border-left: 2px solid var(--primary-1);
+ border-left: 2px solid var(--color-info-default);
}
&--success {
- border-left: 2px solid var(--success-1);
+ border-left: 2px solid var(--color-success-default);
}
& .info-icon {
diff --git a/ui/components/ui/callout/callout.stories.js b/ui/components/ui/callout/callout.stories.js
index 8ee2eaadc..94ec67d92 100644
--- a/ui/components/ui/callout/callout.stories.js
+++ b/ui/components/ui/callout/callout.stories.js
@@ -22,7 +22,7 @@ export default {
};
export const PersistentCallout = (args) => (
-
+
This is your private key:
@@ -36,7 +36,7 @@ export const PersistentCallout = (args) => (
export const DismissibleCallout = (args) => {
const [dismissed, setDismissed] = useState(false);
return (
-
+
This is your private key:
@@ -81,7 +81,7 @@ export const MultipleDismissibleCallouts = () => {
};
return (
-
+
This is your private key:
diff --git a/ui/components/ui/card/README.mdx b/ui/components/ui/card/README.mdx
index 88110ef1d..ddbe0eba2 100644
--- a/ui/components/ui/card/README.mdx
+++ b/ui/components/ui/card/README.mdx
@@ -38,5 +38,5 @@ import { COLORS } from '../../../helpers/constants/design-system';
// All padding related props of the Box component will work
// To change the background color
-
+
```
diff --git a/ui/components/ui/card/card.js b/ui/components/ui/card/card.js
index fc1beafaf..e4ecce4b0 100644
--- a/ui/components/ui/card/card.js
+++ b/ui/components/ui/card/card.js
@@ -11,12 +11,12 @@ import {
const Card = ({
border = true,
padding = 4,
- backgroundColor = COLORS.WHITE,
+ backgroundColor = COLORS.BACKGROUND_DEFAULT,
children,
...props
}) => {
const defaultBorderProps = {
- borderColor: border && COLORS.UI2,
+ borderColor: border && COLORS.BORDER_MUTED,
borderRadius: border && SIZES.MD,
borderStyle: border && BORDER_STYLE.SOLID,
};
diff --git a/ui/components/ui/chip/chip-with-input.js b/ui/components/ui/chip/chip-with-input.js
index 8a5e30cc9..d50c0a4bd 100644
--- a/ui/components/ui/chip/chip-with-input.js
+++ b/ui/components/ui/chip/chip-with-input.js
@@ -7,7 +7,7 @@ import Chip from '.';
export function ChipWithInput({
dataTestId,
className,
- borderColor = COLORS.UI1,
+ borderColor = COLORS.BORDER_DEFAULT,
inputValue,
setInputValue,
}) {
diff --git a/ui/components/ui/chip/chip.js b/ui/components/ui/chip/chip.js
index f84534535..21b4fe13f 100644
--- a/ui/components/ui/chip/chip.js
+++ b/ui/components/ui/chip/chip.js
@@ -9,7 +9,7 @@ export default function Chip({
dataTestId,
className,
children,
- borderColor = COLORS.UI1,
+ borderColor = COLORS.BORDER_DEFAULT,
backgroundColor,
label,
labelProps = {},
@@ -47,7 +47,7 @@ export default function Chip({
className="chip__label"
variant={TYPOGRAPHY.H6}
tag="span"
- color={COLORS.UI4}
+ color={COLORS.TEXT_ALTERNATIVE}
{...labelProps}
>
{label}
diff --git a/ui/components/ui/chip/chip.scss b/ui/components/ui/chip/chip.scss
index 5951a0fa2..7eba44619 100644
--- a/ui/components/ui/chip/chip.scss
+++ b/ui/components/ui/chip/chip.scss
@@ -4,7 +4,7 @@
$self: &;
border-radius: 100px;
- border: 1px solid var(--ui-1);
+ border: 1px solid var(--color-border-default);
padding: 8px 16px;
margin: 0 4px;
display: flex;
@@ -46,6 +46,7 @@
text-align: center;
width: 100%;
font-size: design-system.$font-size-h5;
+ color: var(--color-text-default);
&:focus {
text-align: left;
diff --git a/ui/components/ui/chip/chip.stories.js b/ui/components/ui/chip/chip.stories.js
index fd6f960c5..e2afd944a 100644
--- a/ui/components/ui/chip/chip.stories.js
+++ b/ui/components/ui/chip/chip.stories.js
@@ -1,8 +1,13 @@
import React, { useState } from 'react';
-import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
+import {
+ COLORS,
+ TYPOGRAPHY,
+ SEVERITIES,
+} from '../../../helpers/constants/design-system';
import ApproveIcon from '../icon/approve-icon.component';
+import InfoIcon from '../icon/info-icon.component';
import Identicon from '../identicon/identicon.component';
import { ChipWithInput } from './chip-with-input';
@@ -26,7 +31,9 @@ export default {
},
options: ['ApproveIcon'],
mapping: {
- ApproveIcon: ,
+ ApproveIcon: (
+
+ ),
},
},
rightIcon: {
@@ -86,10 +93,10 @@ DefaultStory.storyName = 'Default';
DefaultStory.args = {
label: 'Chip',
- borderColor: COLORS.UI3,
- backgroundColor: COLORS.UI1,
+ borderColor: COLORS.BORDER_DEFAULT,
+ backgroundColor: COLORS.BACKGROUND_ALTERNATIVE,
labelProps: {
- color: COLORS.BLACK,
+ color: COLORS.TEXT_DEFAULT,
variant: TYPOGRAPHY.H6,
},
};
@@ -97,15 +104,15 @@ DefaultStory.args = {
export const WithLeftIcon = () => (
}
+ borderColor={COLORS.SUCCESS_DEFAULT}
+ leftIcon={ }
/>
);
export const WithRightIcon = () => (
(
export const WithBothIcons = () => (
-
-
- }
+ borderColor={COLORS.BORDER_DEFAULT}
+ rightIcon={ }
leftIcon={
{
};
WithInput.args = {
- borderColor: COLORS.UI3,
+ borderColor: COLORS.BORDER_DEFAULT,
};
diff --git a/ui/components/ui/icon/index.scss b/ui/components/ui/icon/index.scss
index d05d6a433..1cdc11f90 100644
--- a/ui/components/ui/icon/index.scss
+++ b/ui/components/ui/icon/index.scss
@@ -2,18 +2,18 @@
margin: 0 4px;
&--success {
- fill: var(--success-1);
+ fill: var(--color-success-default);
}
&--info {
- fill: var(--primary-1);
+ fill: var(--color-primary-default);
}
&--warning {
- fill: var(--alert-3);
+ fill: var(--color-warning-default);
}
&--danger {
- fill: var(--error-1);
+ fill: var(--color-error-default);
}
}
diff --git a/ui/components/ui/popover/README.mdx b/ui/components/ui/popover/README.mdx
deleted file mode 100644
index 3a4c922db..000000000
--- a/ui/components/ui/popover/README.mdx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
-
-import Popover from './popover.component';
-
-# Popover
-
-A modal component to show info
-
-
-
-
-
-## Component API
-
-
-
diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js
index 4904046bb..09424163e 100644
--- a/ui/components/ui/popover/popover.component.js
+++ b/ui/components/ui/popover/popover.component.js
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useI18nContext } from '../../../hooks/useI18nContext';
+import Box from '../box';
const Popover = ({
title,
@@ -18,9 +19,47 @@ const Popover = ({
CustomBackground,
popoverRef,
centerTitle,
+ headerProps = {},
+ contentProps = {},
+ footerProps = {},
}) => {
const t = useI18nContext();
const showHeader = title || onBack || subtitle || onClose;
+ const Header = () => {
+ return (
+
+
+
+ {onBack ? (
+
+ ) : null}
+ {title}
+
+ {onClose ? (
+
+ ) : null}
+
+ {subtitle ? (
+ {subtitle}
+ ) : null}
+
+ );
+ };
+
return (
{CustomBackground ? (
@@ -33,47 +72,22 @@ const Popover = ({
ref={popoverRef}
>
{showArrow ?
: null}
- {showHeader && (
-
- )}
+ {showHeader &&
}
{children ? (
-
+
{children}
-
+
) : null}
{footer ? (
-
+
) : null}
@@ -132,6 +146,18 @@ Popover.propTypes = {
* Check if use centered title
*/
centerTitle: PropTypes.bool,
+ /**
+ * Box props for the header
+ */
+ headerProps: PropTypes.shape({ ...Box.propTypes }),
+ /**
+ * Box props for the content
+ */
+ contentProps: PropTypes.shape({ ...Box.propTypes }),
+ /**
+ * Box props for the footer
+ */
+ footerProps: PropTypes.shape({ ...Box.propTypes }),
};
export default class PopoverPortal extends PureComponent {
diff --git a/ui/components/ui/popover/popover.stories.js b/ui/components/ui/popover/popover.stories.js
index e23bd91c4..ef9f8e041 100644
--- a/ui/components/ui/popover/popover.stories.js
+++ b/ui/components/ui/popover/popover.stories.js
@@ -1,18 +1,12 @@
import React, { useState } from 'react';
import Button from '../button';
import Box from '../box';
-import README from './README.mdx';
import Popover from './popover.component';
export default {
title: 'Components/UI/Popover',
id: __filename,
component: Popover,
- parameters: {
- docs: {
- page: README,
- },
- },
argTypes: {
title: { control: 'text' },
subtitle: { control: 'text' },
@@ -26,6 +20,21 @@ export default {
showArrow: { control: 'boolean' },
popoverRef: { control: 'object' },
centerTitle: { control: 'boolean' },
+ headerProps: {
+ control: 'object',
+ description:
+ 'Box component props used to add container CSS for the header',
+ },
+ contentProps: {
+ control: 'object',
+ description:
+ 'Box component props used to add container CSS for the content',
+ },
+ footerProps: {
+ control: 'object',
+ description:
+ 'Box component props used to add container CSS for the footer',
+ },
},
};
diff --git a/ui/components/ui/search-icon/search-icon.component.js b/ui/components/ui/search-icon/search-icon.component.js
index 957c499bf..fd3d78081 100644
--- a/ui/components/ui/search-icon/search-icon.component.js
+++ b/ui/components/ui/search-icon/search-icon.component.js
@@ -1,13 +1,9 @@
import React from 'react';
+import PropTypes from 'prop-types';
-export default function SearchIcon() {
+export default function SearchIcon({ color }) {
return (
-
+
@@ -15,3 +11,10 @@ export default function SearchIcon() {
);
}
+
+SearchIcon.propTypes = {
+ /**
+ * Color of the icon should be a valid design system color and is required
+ */
+ color: PropTypes.string.isRequired,
+};
diff --git a/ui/components/ui/text-field/text-field.component.js b/ui/components/ui/text-field/text-field.component.js
index 44a96ee3f..43ef47914 100644
--- a/ui/components/ui/text-field/text-field.component.js
+++ b/ui/components/ui/text-field/text-field.component.js
@@ -88,6 +88,7 @@ const getMaterialThemeInputProps = ({
dir,
classes: { materialLabel, materialFocused, materialError, materialUnderline },
startAdornment,
+ endAdornment,
min,
max,
autoComplete,
@@ -101,6 +102,7 @@ const getMaterialThemeInputProps = ({
},
InputProps: {
startAdornment,
+ endAdornment,
classes: {
underline: materialUnderline,
},
@@ -122,12 +124,14 @@ const getMaterialWhitePaddedThemeInputProps = ({
materialWhitePaddedUnderline,
},
startAdornment,
+ endAdornment,
min,
max,
autoComplete,
}) => ({
InputProps: {
startAdornment,
+ endAdornment,
classes: {
root: materialWhitePaddedRoot,
focused: materialWhitePaddedFocused,
@@ -157,6 +161,7 @@ const getBorderedThemeInputProps = ({
},
largeLabel,
startAdornment,
+ endAdornment,
min,
max,
autoComplete,
@@ -172,6 +177,7 @@ const getBorderedThemeInputProps = ({
},
InputProps: {
startAdornment,
+ endAdornment,
disableUnderline: true,
classes: {
root: inputRoot,
@@ -198,6 +204,7 @@ const TextField = ({
classes,
theme,
startAdornment,
+ endAdornment,
largeLabel,
dir,
min,
@@ -209,6 +216,7 @@ const TextField = ({
const inputProps = themeToInputProps[theme]({
classes,
startAdornment,
+ endAdornment,
largeLabel,
dir,
min,
@@ -257,6 +265,7 @@ TextField.propTypes = {
*/
theme: PropTypes.oneOf(['bordered', 'material', 'material-white-padded']),
startAdornment: PropTypes.element,
+ endAdornment: PropTypes.element,
/**
* Show large label
*/
diff --git a/ui/components/ui/textarea/index.scss b/ui/components/ui/textarea/index.scss
index 1d84362be..7e9c97925 100644
--- a/ui/components/ui/textarea/index.scss
+++ b/ui/components/ui/textarea/index.scss
@@ -3,7 +3,7 @@
.textarea {
display: block;
box-shadow: none;
- color: var(--black);
+ color: var(--color-text-default);
@include design-system.H6;
diff --git a/ui/components/ui/textarea/textarea.js b/ui/components/ui/textarea/textarea.js
index e51f715a0..e20f45052 100644
--- a/ui/components/ui/textarea/textarea.js
+++ b/ui/components/ui/textarea/textarea.js
@@ -34,7 +34,8 @@ const TextArea = ({
);
return (
+ e.section !== t('enableOpenSeaAPI') &&
+ e.section !== t('useCollectibleDetection'),
+ );
+ }
+ return settings;
+}
+
+export function getSettingsRoutes(t) {
+ const settingsRoutesList = [
+ {
+ tab: t('general'),
+ section: t('currencyConversion'),
+ description: '',
+ route: `${GENERAL_ROUTE}#currency-conversion`,
+ image: 'general-icon.svg',
+ id: 1,
+ },
+ {
+ tab: t('general'),
+ section: t('primaryCurrencySetting'),
+ description: t('primaryCurrencySettingDescription'),
+ route: `${GENERAL_ROUTE}#primary-currency`,
+ image: 'general-icon.svg',
+ id: 2,
+ },
+ {
+ tab: t('general'),
+ section: t('currentLanguage'),
+ description: '',
+ route: `${GENERAL_ROUTE}#current-language`,
+ image: 'general-icon.svg',
+ id: 3,
+ },
+ {
+ tab: t('general'),
+ section: t('accountIdenticon'),
+ description: '',
+ route: `${GENERAL_ROUTE}#account-identicon`,
+ image: 'general-icon.svg',
+ id: 4,
+ },
+ {
+ tab: t('general'),
+ section: t('hideZeroBalanceTokens'),
+ description: '',
+ route: `${GENERAL_ROUTE}#zero-balancetokens`,
+ image: 'general-icon.svg',
+ id: 5,
+ },
+ {
+ tab: t('advanced'),
+ section: t('stateLogs'),
+ description: t('stateLogsDescription'),
+ route: `${ADVANCED_ROUTE}#state-logs`,
+ image: 'advanced-icon.svg',
+ id: 6,
+ },
+ {
+ tab: t('advanced'),
+ section: t('syncWithMobile'),
+ description: '',
+ route: `${ADVANCED_ROUTE}#sync-withmobile`,
+ image: 'advanced-icon.svg',
+ id: 7,
+ },
+ {
+ tab: t('advanced'),
+ section: t('resetAccount'),
+ description: t('resetAccountDescription'),
+ route: `${ADVANCED_ROUTE}#reset-account`,
+ image: 'advanced-icon.svg',
+ id: 8,
+ },
+ {
+ tab: t('advanced'),
+ section: t('showAdvancedGasInline'),
+ description: t('showAdvancedGasInlineDescription'),
+ route: `${ADVANCED_ROUTE}#advanced-gascontrols`,
+ image: 'advanced-icon.svg',
+ id: 9,
+ },
+ {
+ tab: t('advanced'),
+ section: t('showHexData'),
+ description: t('showHexDataDescription'),
+ route: `${ADVANCED_ROUTE}#show-hexdata`,
+ image: 'advanced-icon.svg',
+ id: 10,
+ },
+ {
+ tab: t('advanced'),
+ section: t('showFiatConversionInTestnets'),
+ description: t('showFiatConversionInTestnetsDescription'),
+ route: `${ADVANCED_ROUTE}#conversion-testnetworks`,
+ image: 'advanced-icon.svg',
+ id: 11,
+ },
+ {
+ tab: t('advanced'),
+ section: t('showTestnetNetworks'),
+ description: t('showTestnetNetworksDescription'),
+ route: `${ADVANCED_ROUTE}#show-testnets`,
+ image: 'advanced-icon.svg',
+ id: 12,
+ },
+ {
+ tab: t('advanced'),
+ section: t('nonceField'),
+ description: t('nonceFieldDescription'),
+ route: `${ADVANCED_ROUTE}#customize-nonce`,
+ image: 'advanced-icon.svg',
+ id: 13,
+ },
+ {
+ tab: t('advanced'),
+ section: t('autoLockTimeLimit'),
+ description: t('autoLockTimeLimitDescription'),
+ route: `${ADVANCED_ROUTE}#autolock-timer`,
+ image: 'advanced-icon.svg',
+ id: 14,
+ },
+ {
+ tab: t('advanced'),
+ section: t('syncWithThreeBox'),
+ description: t('syncWithThreeBoxDescription'),
+ route: `${ADVANCED_ROUTE}#sync-with3box`,
+ image: 'advanced-icon.svg',
+ id: 15,
+ },
+ {
+ tab: t('advanced'),
+ section: t('ipfsGateway'),
+ description: t('ipfsGatewayDescription'),
+ route: `${ADVANCED_ROUTE}#ipfs-gateway`,
+ image: 'advanced-icon.svg',
+ id: 16,
+ },
+ {
+ tab: t('advanced'),
+ section: t('preferredLedgerConnectionType'),
+ description: t('preferredLedgerConnectionType'),
+ route: `${ADVANCED_ROUTE}#ledger-connection`,
+ image: 'advanced-icon.svg',
+ id: 17,
+ },
+ {
+ tab: t('advanced'),
+ section: t('dismissReminderField'),
+ description: t('dismissReminderDescriptionField'),
+ route: `${ADVANCED_ROUTE}#dimiss-secretrecovery`,
+ image: 'advanced-icon.svg',
+ id: 18,
+ },
+ {
+ tab: t('contacts'),
+ section: t('contacts'),
+ description: t('contacts'),
+ route: CONTACT_LIST_ROUTE,
+ image: 'contacts-icon.svg',
+ id: 19,
+ },
+ {
+ tab: t('securityAndPrivacy'),
+ section: t('revealSeedWords'),
+ description: t('revealSeedWords'),
+ route: `${SECURITY_ROUTE}#reveal-secretrecovery`,
+ image: 'security-icon.svg',
+ id: 20,
+ },
+ {
+ tab: t('securityAndPrivacy'),
+ section: t('showIncomingTransactions'),
+ description: t('showIncomingTransactionsDescription'),
+ route: `${SECURITY_ROUTE}#incoming-transaction`,
+ image: 'security-icon.svg',
+ id: 21,
+ },
+ {
+ tab: t('securityAndPrivacy'),
+ section: t('usePhishingDetection'),
+ description: t('usePhishingDetectionDescription'),
+ route: `${SECURITY_ROUTE}#phishing-detection`,
+ image: 'security-icon.svg',
+ id: 22,
+ },
+ {
+ tab: t('securityAndPrivacy'),
+ section: t('participateInMetaMetrics'),
+ description: t('participateInMetaMetricsDescription'),
+ route: `${SECURITY_ROUTE}#metrametrics`,
+ image: 'security-icon.svg',
+ id: 23,
+ },
+ {
+ tab: t('alerts'),
+ section: t('alertSettingsUnconnectedAccount'),
+ description: t('alertSettingsUnconnectedAccount'),
+ route: `${ALERTS_ROUTE}#unconnected-account`,
+ image: 'alerts-icon.svg',
+ id: 24,
+ },
+ {
+ tab: t('alerts'),
+ section: t('alertSettingsWeb3ShimUsage'),
+ description: t('alertSettingsWeb3ShimUsage'),
+ route: `${ALERTS_ROUTE}#web3-shimusage`,
+ image: 'alerts-icon.svg',
+ id: 25,
+ },
+ {
+ tab: t('networks'),
+ section: t('mainnet'),
+ description: t('mainnet'),
+ route: `${NETWORKS_ROUTE}#networks-mainnet`,
+ image: 'network-icon.svg',
+ id: 26,
+ },
+ {
+ tab: t('networks'),
+ section: t('ropsten'),
+ description: t('ropsten'),
+ route: `${NETWORKS_ROUTE}#networks-ropsten`,
+ image: 'network-icon.svg',
+ id: 27,
+ },
+ {
+ tab: t('networks'),
+ section: t('rinkeby'),
+ description: t('rinkeby'),
+ route: `${NETWORKS_ROUTE}#networks-rinkeby`,
+ image: 'network-icon.svg',
+ id: 28,
+ },
+ {
+ tab: t('networks'),
+ section: t('goerli'),
+ description: t('goerli'),
+ route: `${NETWORKS_ROUTE}#networks-goerli`,
+ image: 'network-icon.svg',
+ id: 29,
+ },
+ {
+ tab: t('networks'),
+ section: t('kovan'),
+ description: t('kovan'),
+ route: `${NETWORKS_ROUTE}#networtks-kovan`,
+ image: 'network-icon.svg',
+ id: 30,
+ },
+ {
+ tab: t('networks'),
+ section: t('localhost'),
+ description: t('localhost'),
+ route: `${NETWORKS_ROUTE}#network-localhost`,
+ image: 'network-icon.svg',
+ id: 31,
+ },
+ {
+ tab: t('experimental'),
+ section: t('useTokenDetection'),
+ description: t('useTokenDetectionDescription'),
+ route: `${EXPERIMENTAL_ROUTE}#token-description`,
+ image: 'experimental-icon.svg',
+ id: 32,
+ },
+ {
+ tab: t('experimental'),
+ section: t('enableOpenSeaAPI'),
+ description: t('enableOpenSeaAPIDescription'),
+ route: `${EXPERIMENTAL_ROUTE}#opensea-api`,
+ image: 'experimental-icon.svg',
+ id: 33,
+ },
+ {
+ tab: t('experimental'),
+ section: t('useCollectibleDetection'),
+ description: t('useCollectibleDetectionDescription'),
+ route: `${EXPERIMENTAL_ROUTE}#autodetect-nfts`,
+ image: 'experimental-icon.svg',
+ id: 34,
+ },
+
+ {
+ tab: t('about'),
+ section: t('metamaskVersion'),
+ description: t('builtAroundTheWorld'),
+ route: `${ABOUT_US_ROUTE}#version`,
+ image: 'info-icon.svg',
+ id: 35,
+ },
+ {
+ tab: t('about'),
+ section: t('links'),
+ description: '',
+ route: `${ABOUT_US_ROUTE}#links`,
+ image: 'info-icon.svg',
+ id: 36,
+ },
+ {
+ tab: t('about'),
+ section: t('privacyMsg'),
+ description: t('privacyMsg'),
+ route: `${ABOUT_US_ROUTE}#privacy-policy`,
+ image: 'info-icon.svg',
+ id: 37,
+ },
+ {
+ tab: t('about'),
+ section: t('terms'),
+ description: t('terms'),
+ route: `${ABOUT_US_ROUTE}#terms`,
+ image: 'info-icon.svg',
+ id: 38,
+ },
+
+ {
+ tab: t('about'),
+ section: t('attributions'),
+ description: t('attributions'),
+ route: `${ABOUT_US_ROUTE}#attributions`,
+ image: 'info-icon.svg',
+ id: 39,
+ },
+
+ {
+ tab: t('about'),
+ section: t('supportCenter'),
+ description: t('supportCenter'),
+ route: `${ABOUT_US_ROUTE}#supportcenter`,
+ image: 'info-icon.svg',
+ id: 40,
+ },
+
+ {
+ tab: t('about'),
+ section: t('visitWebSite'),
+ description: t('visitWebSite'),
+ route: `${ABOUT_US_ROUTE}#visitwebsite`,
+ image: 'info-icon.svg',
+ id: 41,
+ },
+
+ {
+ tab: t('about'),
+ section: t('contactUs'),
+ description: t('contactUs'),
+ route: `${ABOUT_US_ROUTE}#contactus`,
+ image: 'info-icon.svg',
+ id: 42,
+ },
+ ];
+
+ // TODO: write to json file?
+ return showHideSettings(t, settingsRoutesList);
+}
+
+function getFilteredSettingsRoutes(t, tabName) {
+ return getSettingsRoutes(t).filter((s) => s.tab === tabName);
+}
+
+export function getSettingsSectionNumber(t, tabName) {
+ return getSettingsRoutes(t).filter((s) => s.tab === tabName).length;
+}
+
+export function handleSettingsRefs(t, tabName, settingsRefs) {
+ const settingsSearchJsonFiltered = getFilteredSettingsRoutes(t, tabName);
+ const settingsRefsIndex = settingsSearchJsonFiltered.findIndex(
+ (s) => s.route.substring(1) === window.location.hash.substring(1),
+ );
+
+ if (
+ settingsRefsIndex !== -1 &&
+ settingsRefs[settingsRefsIndex].current !== null
+ ) {
+ settingsRefs[settingsRefsIndex].current.scrollIntoView({
+ behavior: 'smooth',
+ });
+ settingsRefs[settingsRefsIndex].current.focus();
+ const historySettingsUrl = window.location.hash.split('#')[1];
+ window.location.hash = historySettingsUrl;
+ }
+}
+
+export function handleHooksSettingsRefs(t, tabName, settingsRefs, itemIndex) {
+ const settingsSearchJsonFiltered = getFilteredSettingsRoutes(t, tabName);
+ const settingsRefsIndex = settingsSearchJsonFiltered.findIndex(
+ (s) => s.route.substring(1) === window.location.hash.substring(1),
+ );
+
+ if (
+ settingsRefsIndex !== -1 &&
+ settingsRefs !== null &&
+ itemIndex === settingsRefsIndex
+ ) {
+ settingsRefs.current.scrollIntoView({
+ behavior: 'smooth',
+ });
+ settingsRefs.current.focus();
+ const historySettingsUrl = window.location.hash.split('#')[1];
+ window.location.hash = historySettingsUrl;
+ }
+}
+
+function colorText(menuElement, regex) {
+ if (menuElement !== null) {
+ let elemText = menuElement?.innerHTML;
+ elemText = elemText.replace('&', '&');
+ elemText = elemText.replace(
+ /(|<\/span>)/gim,
+ '',
+ );
+ menuElement.innerHTML = elemText.replace(
+ regex,
+ '$& ',
+ );
+ }
+}
+export function highlightSearchedText() {
+ const searchElem = document.getElementById('search-settings');
+ const searchRegex = new RegExp(searchElem.value, 'gi');
+ const results = document.querySelectorAll(
+ '.settings-page__header__search__list__item',
+ );
+
+ [...results].forEach((element) => {
+ const menuTabElement = element.querySelector(
+ '.settings-page__header__search__list__item__tab',
+ );
+ const menuSectionElement = element.querySelector(
+ '.settings-page__header__search__list__item__section',
+ );
+
+ colorText(menuTabElement, searchRegex);
+ colorText(menuSectionElement, searchRegex);
+ });
+}
diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js
new file mode 100644
index 000000000..c41eaf757
--- /dev/null
+++ b/ui/helpers/utils/settings-search.test.js
@@ -0,0 +1,564 @@
+import React from 'react';
+import {
+ getSettingsRoutes,
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from './settings-search';
+
+const t = (key) => {
+ switch (key) {
+ case 'general':
+ return 'General';
+ case 'currencyConversion':
+ return 'Currency Conversion';
+ case 'primaryCurrencySetting':
+ return 'Primary Currenc';
+ case 'primaryCurrencySettingDescription':
+ return 'Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency.';
+ case 'currentLanguage':
+ return 'Current Language';
+ case 'accountIdenticon':
+ return 'Current Language"';
+ case 'hideZeroBalanceTokens':
+ return 'Hide Tokens Without Balance';
+ case 'advanced':
+ return 'Advanced';
+ case 'stateLogs':
+ return 'State Logs';
+ case 'stateLogsDescription':
+ return 'State logs contain your public account addresses and sent transactions.';
+ case 'syncWithMobile':
+ return 'Sync with mobile';
+ case 'resetAccount':
+ return 'Reset Account';
+ case 'resetAccountDescription':
+ return 'Resetting your account will clear your transaction history. This will not change the balances in your accounts or require you to re-enter your Secret Recovery Phrase.';
+ case 'showAdvancedGasInline':
+ return 'Advanced gas controls';
+ case 'showAdvancedGasInlineDescription':
+ return 'Select this to show gas price and limit controls directly on the send and confirm screens.';
+ case 'showHexData':
+ return 'Show Hex Data';
+ case 'showHexDataDescription':
+ return 'Select this to show the hex data field on the send screen';
+ case 'showFiatConversionInTestnets':
+ return 'Show Conversion on test networks';
+ case 'showFiatConversionInTestnetsDescription':
+ return 'Select this to show fiat conversion on test network';
+ case 'showTestnetNetworks':
+ return 'Show test networks';
+ case 'showTestnetNetworksDescription':
+ return 'Select this to show test networks in network list';
+ case 'nonceField':
+ return 'Customize transaction nonce';
+ case 'nonceFieldDescription':
+ return 'Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously.';
+ case 'autoLockTimeLimit':
+ return 'Auto-Lock Timer (minutes)';
+ case 'autoLockTimeLimitDescription':
+ return 'Set the idle time in minutes before MetaMask will become locked.';
+ case 'syncWithThreeBox':
+ return 'Sync data with 3Box (experimental)';
+ case 'syncWithThreeBoxDescription':
+ return 'Turn on to have your settings backed up with 3Box. This feature is currently experimental; use at your own risk.';
+ case 'ipfsGateway':
+ return 'IPFS Gateway';
+ case 'ipfsGatewayDescription':
+ return 'Enter the URL of the IPFS CID gateway to use for ENS content resolution.';
+ case 'preferredLedgerConnectionType':
+ return 'Preferred Ledger Connection Type';
+ case 'dismissReminderField':
+ return 'Dismiss Secret Recovery Phrase backup reminder';
+ case 'dismissReminderDescriptionField':
+ return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds';
+ case 'Contacts':
+ return 'Contacts';
+ case 'securityAndPrivacy':
+ return 'Security & Privacy';
+ case 'revealSeedWords':
+ return 'Reveal Secret Recovery Phrase';
+ case 'showIncomingTransactions':
+ return 'Show Incoming Transactions';
+ case 'showIncomingTransactionsDescription':
+ return 'Select this to use Etherscan to show incoming transactions in the transactions list';
+ case 'usePhishingDetection':
+ return 'Use Phishing Detection';
+ case 'usePhishingDetectionDescription':
+ return 'Display a warning for phishing domains targeting Ethereum users';
+ case 'participateInMetaMetrics':
+ return 'Participate in MetaMetrics';
+ case 'participateInMetaMetricsDescription':
+ return 'Participate in MetaMetrics to help us make MetaMask better';
+ case 'alerts':
+ return 'Alerts';
+ case 'alertSettingsUnconnectedAccount':
+ return 'Browsing a website with an unconnected account selected';
+ case 'alertSettingsWeb3ShimUsage':
+ return 'When a website tries to use the removed window.web3 API';
+ case 'networks':
+ return 'Networks';
+ case 'mainnet':
+ return 'Ethereum Mainnet';
+ case 'ropsten':
+ return 'Ropsten Test Network';
+ case 'rinkeby':
+ return 'Rinkeby Test Network';
+ case 'goerli':
+ return 'Goerli Test Network';
+ case 'kovan':
+ return 'Kovan Test Network';
+ case 'localhost':
+ return 'Localhost 8545';
+ case 'experimental':
+ return 'Experimental';
+ case 'useTokenDetection':
+ return 'Use Token Detection';
+ case 'useTokenDetectionDescription':
+ return 'We use third-party APIs to detect and display new tokens sent to your wallet. Turn off if you don’t want MetaMask to pull data from those services.';
+ case 'enableOpenSeaAPI':
+ return 'Enable OpenSea API';
+ case 'enableOpenSeaAPIDescription':
+ return 'Use OpenSea API to fetch NFT data.NFT auto - detection relies on OpenSea API, and will not be available when this is turned off.';
+ case 'useCollectibleDetection':
+ return 'Autodetect NFTs';
+ case 'useCollectibleDetectionDescription':
+ return 'Displaying NFTs media & data may expose your IP address to centralized servers. Third-party APIs (like OpenSea) are used to detect NFTs in your wallet. This exposes your account address with those services. Leave this disabled if you don’t want the app to pull data from those those services.';
+ case 'about':
+ return 'About';
+ case 'metamaskVersion':
+ return 'MetaMask Version';
+ case 'builtAroundTheWorld':
+ return 'MetaMask is designed and built around the world.';
+ case 'links':
+ return 'Links';
+ case 'privacyMsg':
+ return 'Privacy Policy';
+ case 'terms':
+ return 'Terms of Use';
+ case 'attributions':
+ return 'Attributions';
+ case 'supportCenter':
+ return 'Visit our Support Center';
+ case 'visitWebSite':
+ return 'Visit our web site';
+ case 'contactUs':
+ return 'Contact us';
+
+ default:
+ return '';
+ }
+};
+
+describe('Settings Search Utils', () => {
+ describe('getSettingsRoutes', () => {
+ it('should get all settings', () => {
+ const settingsListExcepted = [
+ {
+ description: '',
+ id: 1,
+ image: 'general-icon.svg',
+ route: '/settings/general#currency-conversion',
+ section: 'Currency Conversion',
+ tab: 'General',
+ },
+ {
+ description:
+ 'Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency.',
+ id: 2,
+ image: 'general-icon.svg',
+ route: '/settings/general#primary-currency',
+ section: 'Primary Currenc',
+ tab: 'General',
+ },
+ {
+ description: '',
+ id: 3,
+ image: 'general-icon.svg',
+ route: '/settings/general#current-language',
+ section: 'Current Language',
+ tab: 'General',
+ },
+ {
+ description: '',
+ id: 4,
+ image: 'general-icon.svg',
+ route: '/settings/general#account-identicon',
+ section: 'Current Language"',
+ tab: 'General',
+ },
+ {
+ description: '',
+ id: 5,
+ image: 'general-icon.svg',
+ route: '/settings/general#zero-balancetokens',
+ section: 'Hide Tokens Without Balance',
+ tab: 'General',
+ },
+ {
+ description:
+ 'State logs contain your public account addresses and sent transactions.',
+ id: 6,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#state-logs',
+ section: 'State Logs',
+ tab: 'Advanced',
+ },
+ {
+ description: '',
+ id: 7,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#sync-withmobile',
+ section: 'Sync with mobile',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Resetting your account will clear your transaction history. This will not change the balances in your accounts or require you to re-enter your Secret Recovery Phrase.',
+ id: 8,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#reset-account',
+ section: 'Reset Account',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Select this to show gas price and limit controls directly on the send and confirm screens.',
+ id: 9,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#advanced-gascontrols',
+ section: 'Advanced gas controls',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Select this to show the hex data field on the send screen',
+ id: 10,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#show-hexdata',
+ section: 'Show Hex Data',
+ tab: 'Advanced',
+ },
+ {
+ description: 'Select this to show fiat conversion on test network',
+ id: 11,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#conversion-testnetworks',
+ section: 'Show Conversion on test networks',
+ tab: 'Advanced',
+ },
+ {
+ description: 'Select this to show test networks in network list',
+ id: 12,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#show-testnets',
+ section: 'Show test networks',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously.',
+ id: 13,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#customize-nonce',
+ section: 'Customize transaction nonce',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Set the idle time in minutes before MetaMask will become locked.',
+ id: 14,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#autolock-timer',
+ section: 'Auto-Lock Timer (minutes)',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Turn on to have your settings backed up with 3Box. This feature is currently experimental; use at your own risk.',
+ id: 15,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#sync-with3box',
+ section: 'Sync data with 3Box (experimental)',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Enter the URL of the IPFS CID gateway to use for ENS content resolution.',
+ id: 16,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#ipfs-gateway',
+ section: 'IPFS Gateway',
+ tab: 'Advanced',
+ },
+ {
+ description: 'Preferred Ledger Connection Type',
+ id: 17,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#ledger-connection',
+ section: 'Preferred Ledger Connection Type',
+ tab: 'Advanced',
+ },
+ {
+ description:
+ 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds',
+ id: 18,
+ image: 'advanced-icon.svg',
+ route: '/settings/advanced#dimiss-secretrecovery',
+ section: 'Dismiss Secret Recovery Phrase backup reminder',
+ tab: 'Advanced',
+ },
+ {
+ description: '',
+ id: 19,
+ image: 'contacts-icon.svg',
+ route: '/settings/contact-list',
+ section: '',
+ tab: '',
+ },
+ {
+ description: 'Reveal Secret Recovery Phrase',
+ id: 20,
+ image: 'security-icon.svg',
+ route: '/settings/security#reveal-secretrecovery',
+ section: 'Reveal Secret Recovery Phrase',
+ tab: 'Security & Privacy',
+ },
+ {
+ description:
+ 'Select this to use Etherscan to show incoming transactions in the transactions list',
+ id: 21,
+ image: 'security-icon.svg',
+ route: '/settings/security#incoming-transaction',
+ section: 'Show Incoming Transactions',
+ tab: 'Security & Privacy',
+ },
+ {
+ description:
+ 'Display a warning for phishing domains targeting Ethereum users',
+ id: 22,
+ image: 'security-icon.svg',
+ route: '/settings/security#phishing-detection',
+ section: 'Use Phishing Detection',
+ tab: 'Security & Privacy',
+ },
+ {
+ description:
+ 'Participate in MetaMetrics to help us make MetaMask better',
+ id: 23,
+ image: 'security-icon.svg',
+ route: '/settings/security#metrametrics',
+ section: 'Participate in MetaMetrics',
+ tab: 'Security & Privacy',
+ },
+ {
+ description:
+ 'Browsing a website with an unconnected account selected',
+ id: 24,
+ image: 'alerts-icon.svg',
+ route: '/settings/alerts#unconnected-account',
+ section: 'Browsing a website with an unconnected account selected',
+ tab: 'Alerts',
+ },
+ {
+ description:
+ 'When a website tries to use the removed window.web3 API',
+ id: 25,
+ image: 'alerts-icon.svg',
+ route: '/settings/alerts#web3-shimusage',
+ section: 'When a website tries to use the removed window.web3 API',
+ tab: 'Alerts',
+ },
+ {
+ description: 'Ethereum Mainnet',
+ id: 26,
+ image: 'network-icon.svg',
+ route: '/settings/networks#networks-mainnet',
+ section: 'Ethereum Mainnet',
+ tab: 'Networks',
+ },
+ {
+ description: 'Ropsten Test Network',
+ id: 27,
+ image: 'network-icon.svg',
+ route: '/settings/networks#networks-ropsten',
+ section: 'Ropsten Test Network',
+ tab: 'Networks',
+ },
+ {
+ description: 'Rinkeby Test Network',
+ id: 28,
+ image: 'network-icon.svg',
+ route: '/settings/networks#networks-rinkeby',
+ section: 'Rinkeby Test Network',
+ tab: 'Networks',
+ },
+ {
+ description: 'Goerli Test Network',
+ id: 29,
+ image: 'network-icon.svg',
+ route: '/settings/networks#networks-goerli',
+ section: 'Goerli Test Network',
+ tab: 'Networks',
+ },
+ {
+ description: 'Kovan Test Network',
+ id: 30,
+ image: 'network-icon.svg',
+ route: '/settings/networks#networtks-kovan',
+ section: 'Kovan Test Network',
+ tab: 'Networks',
+ },
+ {
+ description: 'Localhost 8545',
+ id: 31,
+ image: 'network-icon.svg',
+ route: '/settings/networks#network-localhost',
+ section: 'Localhost 8545',
+ tab: 'Networks',
+ },
+ {
+ description:
+ 'We use third-party APIs to detect and display new tokens sent to your wallet. Turn off if you don’t want MetaMask to pull data from those services.',
+ id: 32,
+ image: 'experimental-icon.svg',
+ route: '/settings/experimental#token-description',
+ section: 'Use Token Detection',
+ tab: 'Experimental',
+ },
+ {
+ description: 'MetaMask is designed and built around the world.',
+ id: 35,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#version',
+ section: 'MetaMask Version',
+ tab: 'About',
+ },
+ {
+ description: '',
+ id: 36,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#links',
+ section: 'Links',
+ tab: 'About',
+ },
+ {
+ description: 'Privacy Policy',
+ id: 37,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#privacy-policy',
+ section: 'Privacy Policy',
+ tab: 'About',
+ },
+ {
+ description: 'Terms of Use',
+ id: 38,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#terms',
+ section: 'Terms of Use',
+ tab: 'About',
+ },
+ {
+ description: 'Attributions',
+ id: 39,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#attributions',
+ section: 'Attributions',
+ tab: 'About',
+ },
+ {
+ description: 'Visit our Support Center',
+ id: 40,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#supportcenter',
+ section: 'Visit our Support Center',
+ tab: 'About',
+ },
+ {
+ description: 'Visit our web site',
+ id: 41,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#visitwebsite',
+ section: 'Visit our web site',
+ tab: 'About',
+ },
+ {
+ description: 'Contact us',
+ id: 42,
+ image: 'info-icon.svg',
+ route: '/settings/about-us#contactus',
+ section: 'Contact us',
+ tab: 'About',
+ },
+ ];
+ expect(getSettingsRoutes(t)).toStrictEqual(settingsListExcepted);
+ });
+
+ it('should not get all settings', () => {
+ const settingsListExcepted = [
+ {
+ description: '',
+ image: 'general-icon.svg',
+ route: '/settings/general#currency-conversion',
+ section: 'Currency Conversion',
+ tab: 'General',
+ },
+ {
+ description: 'Contact us',
+ image: 'info-icon.svg',
+ route: '/settings/about-us#contactus',
+ section: 'Contact us',
+ tab: 'About',
+ },
+ ];
+ expect(getSettingsRoutes(t)).not.toStrictEqual(settingsListExcepted);
+ });
+ });
+
+ describe('getSettingsSectionNumber', () => {
+ it('should get good general section number', () => {
+ expect(getSettingsSectionNumber(t, t('general'))).toStrictEqual(5);
+ });
+
+ it('should get good advanced section number', () => {
+ expect(getSettingsSectionNumber(t, t('advanced'))).toStrictEqual(13);
+ });
+
+ it('should get good contact section number', () => {
+ expect(getSettingsSectionNumber(t, t('contacts'))).toStrictEqual(1);
+ });
+
+ it('should get good security & privacy section number', () => {
+ expect(
+ getSettingsSectionNumber(t, t('securityAndPrivacy')),
+ ).toStrictEqual(4);
+ });
+
+ it('should get good alerts section number', () => {
+ expect(getSettingsSectionNumber(t, t('alerts'))).toStrictEqual(2);
+ });
+
+ it('should get good network section number', () => {
+ expect(getSettingsSectionNumber(t, t('networks'))).toStrictEqual(6);
+ });
+
+ it('should get good experimental section number', () => {
+ expect(getSettingsSectionNumber(t, t('experimental'))).toStrictEqual(1);
+ });
+
+ it('should get good about section number', () => {
+ expect(getSettingsSectionNumber(t, t('about'))).toStrictEqual(8);
+ });
+ });
+
+ // Can't be tested without DOM element
+ describe('handleSettingsRefs', () => {
+ it('should handle general refs', () => {
+ const settingsRefs = Array(getSettingsSectionNumber(t, t('general')))
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+ expect(handleSettingsRefs(t, t('general'), settingsRefs)).toBeUndefined();
+ });
+ });
+});
diff --git a/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js b/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js
index 77aa8c679..9898d0331 100644
--- a/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js
+++ b/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js
@@ -19,7 +19,10 @@ import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
import { feeParamsAreCustom, getGasFeeEstimate } from './utils';
-const getMaxFeePerGasFromTransaction = (transaction) => {
+const getMaxFeePerGasFromTransaction = (transaction, gasFeeEstimates) => {
+ if (gasFeeEstimates?.[transaction?.userFeeLevel]) {
+ return gasFeeEstimates[transaction.userFeeLevel].suggestedMaxFeePerGas;
+ }
const { maxFeePerGas, gasPrice } = transaction?.txParams || {};
return Number(hexWEIToDecGWEI(maxFeePerGas || gasPrice));
};
@@ -64,7 +67,7 @@ export function useMaxFeePerGasInput({
const showFiat = useSelector(getShouldShowFiat);
const initialMaxFeePerGas = supportsEIP1559
- ? getMaxFeePerGasFromTransaction(transaction)
+ ? getMaxFeePerGasFromTransaction(transaction, gasFeeEstimates)
: 0;
// This hook keeps track of a few pieces of transitional state. It is
diff --git a/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js b/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js
index 87e5b7b50..a3ae7ff4a 100644
--- a/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js
+++ b/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js
@@ -16,7 +16,14 @@ import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
import { feeParamsAreCustom, getGasFeeEstimate } from './utils';
-const getMaxPriorityFeePerGasFromTransaction = (transaction) => {
+const getMaxPriorityFeePerGasFromTransaction = (
+ transaction,
+ gasFeeEstimates,
+) => {
+ if (gasFeeEstimates?.[transaction?.userFeeLevel]) {
+ return gasFeeEstimates[transaction.userFeeLevel]
+ .suggestedMaxPriorityFeePerGas;
+ }
const { maxPriorityFeePerGas, maxFeePerGas, gasPrice } =
transaction?.txParams || {};
return Number(
@@ -64,7 +71,7 @@ export function useMaxPriorityFeePerGasInput({
const showFiat = useSelector(getShouldShowFiat);
const initialMaxPriorityFeePerGas = supportsEIP1559
- ? getMaxPriorityFeePerGasFromTransaction(transaction)
+ ? getMaxPriorityFeePerGasFromTransaction(transaction, gasFeeEstimates)
: 0;
const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState(() => {
diff --git a/ui/hooks/useAddressDetails.js b/ui/hooks/useAddressDetails.js
new file mode 100644
index 000000000..9755f3bf1
--- /dev/null
+++ b/ui/hooks/useAddressDetails.js
@@ -0,0 +1,48 @@
+import { useSelector } from 'react-redux';
+
+import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
+import {
+ getAddressBook,
+ getMetaMaskIdentities,
+ getTokenList,
+ getUseTokenDetection,
+} from '../selectors';
+import { shortenAddress } from '../helpers/utils/util';
+
+const useAddressDetails = (toAddress) => {
+ const addressBook = useSelector(getAddressBook);
+ const identities = useSelector(getMetaMaskIdentities);
+ const tokenList = useSelector(getTokenList);
+ const useTokenDetection = useSelector(getUseTokenDetection);
+ const checksummedAddress = toChecksumHexAddress(toAddress);
+
+ if (!toAddress) {
+ return {};
+ }
+ const addressBookEntryObject = addressBook.find(
+ (entry) => entry.address === checksummedAddress,
+ );
+ if (addressBookEntryObject?.name) {
+ return { toName: addressBookEntryObject.name, isTrusted: true };
+ }
+ if (identities[toAddress]?.name) {
+ return { toName: identities[toAddress].name, isTrusted: true };
+ }
+ const casedTokenList = useTokenDetection
+ ? tokenList
+ : Object.keys(tokenList).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: tokenList[base],
+ };
+ }, {});
+ if (casedTokenList[toAddress]?.name) {
+ return { toName: casedTokenList[toAddress].name, isTrusted: true };
+ }
+ return {
+ toName: shortenAddress(checksummedAddress),
+ isTrusted: false,
+ };
+};
+
+export default useAddressDetails;
diff --git a/ui/hooks/useAddressDetails.test.js b/ui/hooks/useAddressDetails.test.js
new file mode 100644
index 000000000..89fb38d12
--- /dev/null
+++ b/ui/hooks/useAddressDetails.test.js
@@ -0,0 +1,102 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { renderHook } from '@testing-library/react-hooks';
+
+import configureStore from '../store/store';
+import useAddressDetails from './useAddressDetails';
+
+const renderUseAddressDetails = (toAddress, stateVariables = {}) => {
+ const mockState = {
+ metamask: {
+ provider: {
+ type: 'test',
+ chainId: '0x3',
+ },
+ tokenList: {},
+ ...stateVariables,
+ },
+ };
+
+ const wrapper = ({ children }) => (
+ {children}
+ );
+
+ return renderHook(() => useAddressDetails(toAddress), { wrapper });
+};
+
+describe('useAddressDetails', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return empty object if no address is passed', () => {
+ const { result } = renderUseAddressDetails();
+ expect(result.current).toStrictEqual({});
+ });
+
+ it('should return name from addressBook if address is present in addressBook', () => {
+ const { result } = renderUseAddressDetails(
+ '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ {
+ addressBook: {
+ '0x3': {
+ '0x06195827297c7A80a443b6894d3BDB8824b43896': {
+ address: '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ name: 'Address Book Account 1',
+ chainId: '0x3',
+ },
+ },
+ },
+ },
+ );
+ const { toName, isTrusted } = result.current;
+ expect(toName).toBe('Address Book Account 1');
+ expect(isTrusted).toBe(true);
+ });
+
+ it('should return name from identities if address is present in identities', () => {
+ const { result } = renderUseAddressDetails(
+ '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ {
+ identities: {
+ '0x06195827297c7A80a443b6894d3BDB8824b43896': {
+ address: '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ name: 'Account 1',
+ },
+ },
+ },
+ );
+ const { toName, isTrusted } = result.current;
+ expect(toName).toBe('Account 1');
+ expect(isTrusted).toBe(true);
+ });
+
+ it('should return name from tokenlist if address is present in tokens', () => {
+ const { result } = renderUseAddressDetails(
+ '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ {
+ useTokenDetection: true,
+ tokenList: {
+ '0x06195827297c7A80a443b6894d3BDB8824b43896': {
+ address: '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ symbol: 'LINK',
+ decimals: 18,
+ name: 'TOKEN-ABC',
+ },
+ },
+ },
+ );
+ const { toName, isTrusted } = result.current;
+ expect(toName).toBe('TOKEN-ABC');
+ expect(isTrusted).toBe(true);
+ });
+
+ it('should return shortened address if address is not presend in any of above sources', () => {
+ const { result } = renderUseAddressDetails(
+ '0x06195827297c7A80a443b6894d3BDB8824b43896',
+ );
+ const { toName, isTrusted } = result.current;
+ expect(toName).toBe('0x061...3896');
+ expect(isTrusted).toBe(false);
+ });
+});
diff --git a/ui/pages/add-collectible/add-collectible.js b/ui/pages/add-collectible/add-collectible.js
index 188756aa1..0cf1bea74 100644
--- a/ui/pages/add-collectible/add-collectible.js
+++ b/ui/pages/add-collectible/add-collectible.js
@@ -5,7 +5,15 @@ import { util } from '@metamask/controllers';
import { useI18nContext } from '../../hooks/useI18nContext';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
+import {
+ DISPLAY,
+ FONT_WEIGHT,
+ TYPOGRAPHY,
+} from '../../helpers/constants/design-system';
+
import Box from '../../components/ui/box';
+import Typography from '../../components/ui/typography';
+import ActionableMessage from '../../components/ui/actionable-message';
import PageContainer from '../../components/ui/page-container';
import {
addCollectibleVerifyOwnership,
@@ -38,6 +46,7 @@ export default function AddCollectible() {
);
const [tokenId, setTokenId] = useState('');
const [disabled, setDisabled] = useState(true);
+ const [collectibleAddFailed, setCollectibleAddFailed] = useState(false);
const handleAddCollectible = async () => {
try {
@@ -47,7 +56,7 @@ export default function AddCollectible() {
} catch (error) {
const { message } = error;
dispatch(setNewCollectibleAddedMessage(message));
- history.push(DEFAULT_ROUTE);
+ setCollectibleAddFailed(true);
return;
}
if (contractAddressToConvertFromTokenToCollectible) {
@@ -84,24 +93,48 @@ export default function AddCollectible() {
}}
disabled={disabled}
contentComponent={
-
+
{isMainnet &&
!useCollectibleDetection &&
!collectibleDetectionNoticeDismissed ? (
) : null}
-
+ {collectibleAddFailed && (
+
+
+ {t('collectibleAddFailedMessage')}
+
+ setCollectibleAddFailed(false)}
+ />
+
+ }
+ />
+ )}
+
validateAndSetAddress(val)}
+ onChange={(val) => {
+ validateAndSetAddress(val);
+ setCollectibleAddFailed(false);
+ }}
tooltipText={t('importNFTAddressToolTip')}
autoFocus
/>
-
-
{
validateAndSetTokenId(val);
+ setCollectibleAddFailed(false);
}}
tooltipText={t('importNFTTokenIdToolTip')}
numeric
diff --git a/ui/pages/add-collectible/index.scss b/ui/pages/add-collectible/index.scss
new file mode 100644
index 000000000..bd399e477
--- /dev/null
+++ b/ui/pages/add-collectible/index.scss
@@ -0,0 +1,9 @@
+.add-collectible {
+ &__close {
+ color: var(--ui-black);
+ background: none;
+ flex: 0;
+ align-self: flex-start;
+ padding-right: 0;
+ }
+}
diff --git a/ui/pages/confirm-import-token/confirm-import-token.js b/ui/pages/confirm-import-token/confirm-import-token.js
index dcba139b2..ab0cb7152 100644
--- a/ui/pages/confirm-import-token/confirm-import-token.js
+++ b/ui/pages/confirm-import-token/confirm-import-token.js
@@ -1,4 +1,4 @@
-import React, { useCallback, useContext, useEffect, useState } from 'react';
+import React, { useCallback, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import {
@@ -9,9 +9,9 @@ import Button from '../../components/ui/button';
import Identicon from '../../components/ui/identicon';
import TokenBalance from '../../components/ui/token-balance';
import { I18nContext } from '../../contexts/i18n';
+import { MetaMetricsContext as NewMetaMetricsContext } from '../../contexts/metametrics.new';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getPendingTokens } from '../../ducks/metamask/metamask';
-import { useNewMetricEvent } from '../../hooks/useMetricEvent';
import { addTokens, clearPendingTokens } from '../../store/actions';
const getTokenName = (name, symbol) => {
@@ -22,24 +22,11 @@ const ConfirmImportToken = () => {
const t = useContext(I18nContext);
const dispatch = useDispatch();
const history = useHistory();
+ const trackEvent = useContext(NewMetaMetricsContext);
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
const pendingTokens = useSelector(getPendingTokens);
- const [addedToken, setAddedToken] = useState({});
-
- const trackTokenAddedEvent = useNewMetricEvent({
- event: 'Token Added',
- category: 'Wallet',
- sensitiveProperties: {
- token_symbol: addedToken.symbol,
- token_contract_address: addedToken.address,
- token_decimal_precision: addedToken.decimals,
- unlisted: addedToken.unlisted,
- source: addedToken.isCustom ? 'custom' : 'list',
- },
- });
-
const handleAddTokens = useCallback(async () => {
await dispatch(addTokens(pendingTokens));
@@ -47,8 +34,19 @@ const ConfirmImportToken = () => {
const firstTokenAddress = addedTokenValues?.[0].address?.toLowerCase();
addedTokenValues.forEach((pendingToken) => {
- setAddedToken({ ...pendingToken });
+ trackEvent({
+ event: 'Token Added',
+ category: 'Wallet',
+ sensitiveProperties: {
+ token_symbol: pendingToken.symbol,
+ token_contract_address: pendingToken.address,
+ token_decimal_precision: pendingToken.decimals,
+ unlisted: pendingToken.unlisted,
+ source: pendingToken.isCustom ? 'custom' : 'list',
+ },
+ });
});
+
dispatch(clearPendingTokens());
if (firstTokenAddress) {
@@ -56,13 +54,7 @@ const ConfirmImportToken = () => {
} else {
history.push(mostRecentOverviewPage);
}
- }, [dispatch, history, mostRecentOverviewPage, pendingTokens]);
-
- useEffect(() => {
- if (Object.keys(addedToken).length) {
- trackTokenAddedEvent();
- }
- }, [addedToken, trackTokenAddedEvent]);
+ }, [dispatch, history, mostRecentOverviewPage, pendingTokens, trackEvent]);
useEffect(() => {
if (Object.keys(pendingTokens).length === 0) {
diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
index cf2da63f8..99f22d3c5 100644
--- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
+++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
@@ -93,7 +93,7 @@ export default function ConfirmTokenTransactionBase({
toAddress={toAddress}
image={image}
onEdit={onEdit}
- identiconAddress={tokenAddress}
+ tokenAddress={tokenAddress}
title={title}
subtitleComponent={subtitleComponent()}
primaryTotalTextOverride={`${title} + ${ethTransactionTotal} ${nativeCurrency}`}
diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
index ef35c4c72..2fae7c16f 100644
--- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -60,6 +60,7 @@ import {
import Typography from '../../components/ui/typography/typography';
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
+import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import TransactionAlerts from './transaction-alerts';
@@ -113,7 +114,7 @@ export default class ConfirmTransactionBase extends Component {
dataHexComponent: PropTypes.node,
hideData: PropTypes.bool,
hideSubtitle: PropTypes.bool,
- identiconAddress: PropTypes.string,
+ tokenAddress: PropTypes.string,
onEdit: PropTypes.func,
subtitleComponent: PropTypes.node,
title: PropTypes.string,
@@ -144,6 +145,7 @@ export default class ConfirmTransactionBase extends Component {
hardwareWalletRequiresConnection: PropTypes.bool,
isMultiLayerFeeNetwork: PropTypes.bool,
eip1559V2Enabled: PropTypes.bool,
+ showBuyModal: PropTypes.func,
};
state = {
@@ -323,6 +325,7 @@ export default class ConfirmTransactionBase extends Component {
supportsEIP1559,
isMultiLayerFeeNetwork,
nativeCurrency,
+ showBuyModal,
} = this.props;
const { t } = this.context;
const { userAcknowledgedGasMissing } = this.state;
@@ -335,6 +338,7 @@ export default class ConfirmTransactionBase extends Component {
const hasSimulationError = Boolean(txData.simulationFails);
const renderSimulationFailureWarning =
hasSimulationError && !userAcknowledgedGasMissing;
+ const networkName = NETWORK_TO_NAME_MAP[txData.chainId];
const renderTotalMaxAmount = () => {
if (
@@ -582,6 +586,11 @@ export default class ConfirmTransactionBase extends Component {
this.setUserAcknowledgedGasMissing()
}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
+ chainId={txData.chainId}
+ nativeCurrency={nativeCurrency}
+ networkName={networkName}
+ showBuyModal={showBuyModal}
+ type={txData.type}
/>
this.handleCloseEditGas()}
currentTransaction={txData}
supportsEIP1559V2={this.supportsEIP1559V2}
+ nativeCurrency={nativeCurrency}
/>
);
diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
index 8b13a1034..c6a6598ad 100644
--- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
+++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -285,6 +285,7 @@ export const mapDispatchToProps = (dispatch) => {
updateTransactionGasFees: (gasFees) => {
dispatch(updateTransactionGasFees({ ...gasFees, expectHexWei: true }));
},
+ showBuyModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
};
};
diff --git a/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js b/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js
index ae6b195ad..bbb1883ab 100644
--- a/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js
+++ b/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js
@@ -3,19 +3,25 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas';
-import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../helpers/constants/error-keys';
import { submittedPendingTransactionsSelector } from '../../../selectors/transactions';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { useI18nContext } from '../../../hooks/useI18nContext';
import ActionableMessage from '../../../components/ui/actionable-message/actionable-message';
-import ErrorMessage from '../../../components/ui/error-message';
import I18nValue from '../../../components/ui/i18n-value';
+import Button from '../../../components/ui/button';
import Typography from '../../../components/ui/typography';
import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
+import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
+import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
const TransactionAlerts = ({
userAcknowledgedGasMissing,
setUserAcknowledgedGasMissing,
+ chainId,
+ nativeCurrency,
+ networkName,
+ showBuyModal,
+ type,
}) => {
const {
balanceError,
@@ -90,7 +96,45 @@ const TransactionAlerts = ({
type="warning"
/>
)}
- {balanceError && }
+ {balanceError &&
+ chainId === MAINNET_CHAIN_ID &&
+ type === TRANSACTION_TYPES.DEPLOY_CONTRACT ? (
+
+ {t('insufficientCurrency', [nativeCurrency, networkName])}{' '}
+
+ {t('buyEth')}
+ {' '}
+ {t('orDeposit')}
+
+ }
+ useIcon
+ iconFillColor="#d73a49"
+ type="danger"
+ />
+ ) : null}
+ {balanceError &&
+ chainId !== MAINNET_CHAIN_ID &&
+ type === TRANSACTION_TYPES.DEPLOY_CONTRACT ? (
+
+ {t('insufficientCurrency', [nativeCurrency, networkName])}
+ {t('buyOther', [nativeCurrency])}
+
+ }
+ useIcon
+ iconFillColor="#d73a49"
+ type="danger"
+ />
+ ) : null}
{estimateUsed === PRIORITY_LEVELS.LOW && (
{
supportsEIP1559V2: true,
balanceError: true,
},
+ componentProps: {
+ nativeCurrency: 'ETH',
+ networkName: 'Ropsten',
+ showBuyModal: jest.fn(),
+ chainId: '0x1',
+ type: TRANSACTION_TYPES.DEPLOY_CONTRACT,
+ },
});
- expect(getByText('Insufficient funds.')).toBeInTheDocument();
+ expect(
+ getByText(
+ /You do not have enough ETH in your account to pay for transaction fees on Ropsten network./u,
+ ),
+ ).toBeInTheDocument();
});
});
diff --git a/ui/pages/create-account/connect-hardware/select-hardware.js b/ui/pages/create-account/connect-hardware/select-hardware.js
index 375b33f23..be4d1326b 100644
--- a/ui/pages/create-account/connect-hardware/select-hardware.js
+++ b/ui/pages/create-account/connect-hardware/select-hardware.js
@@ -350,6 +350,30 @@ export default class SelectHardware extends Component {
>
),
},
+ {
+ message: (
+ <>
+
+ {this.context.t('airgapVault')}
+
+
+ {this.context.t('airgapVaultTutorial')}
+
+ >
+ ),
+ },
{
message: this.context.t('QRHardwareWalletSteps2Description'),
},
diff --git a/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js b/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js
index 0930eb995..9fbdc0115 100644
--- a/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js
+++ b/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js
@@ -87,7 +87,7 @@ export default function SeedPhraseIntro() {
padding={4}
borderWidth={1}
borderRadius={SIZES.MD}
- borderColor={COLORS.UI2}
+ borderColor={COLORS.BORDER_MUTED}
borderStyle={BORDER_STYLE.SOLID}
>
diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js
index 004312a85..46627c77f 100644
--- a/ui/pages/home/home.component.js
+++ b/ui/pages/home/home.component.js
@@ -303,27 +303,21 @@ export default class Home extends PureComponent {
: null
///: END:ONLY_INCLUDE_IN
}
- {newCollectibleAddedMessage ? (
+ {newCollectibleAddedMessage === 'success' ? (
- {newCollectibleAddedMessage === 'success' ? (
-
- ) : null}
+
- {newCollectibleAddedMessage === 'success'
- ? t('newCollectibleAddedMessage')
- : t('newCollectibleAddFailed', [
- newCollectibleAddedMessage,
- ])}
+ {t('newCollectibleAddedMessage')}
-
-
+
+
+
{
@@ -65,23 +70,57 @@ class RestoreVaultPage extends Component {
>
{`< ${t('back')}`}
-
- {this.context.t('restoreAccountWithSeed')}
-
-
- {this.context.t('secretPhrase')}
-
-
- {this.context.t('secretPhraseWarning')}
-
+
+ {t('resetWallet')}
+
+
+ {t('resetWalletSubHeader')}
+
+
+ {t('resetWalletUsingSRP', [
+
+ {t('reAddAccounts')}
+ ,
+
+ {t('reAdded')}
+ ,
+
+ {t('reAdded')}
+ ,
+ ])}
+
+
+ {t('resetWalletWarning')}
+
-
-
-
+
+
+
);
}
}
diff --git a/ui/pages/keychains/restore-vault.test.js b/ui/pages/keychains/restore-vault.test.js
new file mode 100644
index 000000000..7fde0a8c9
--- /dev/null
+++ b/ui/pages/keychains/restore-vault.test.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import sinon from 'sinon';
+import configureMockStore from 'redux-mock-store';
+import { renderWithProvider } from '../../../test/lib/render-helpers';
+import RestoreVaultPage from './restore-vault';
+
+describe('Restore vault Component', () => {
+ it('clicks imports seed button', () => {
+ const props = {
+ history: {
+ push: sinon.spy(),
+ },
+ };
+
+ const { getByText, getByRole, getAllByRole } = renderWithProvider(
+ ,
+ configureMockStore()({
+ metamask: { currentLocale: 'en' },
+ appState: { isLoading: false },
+ }),
+ );
+
+ expect(getByText('Reset Wallet')).toBeInTheDocument();
+ expect(
+ getByText(
+ 'MetaMask does not keep a copy of your password. If you’re having trouble unlocking your account, you will need to reset your wallet. You can do this by providing the Secret Recovery Phrase you used when you set up your wallet.',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ getByText(
+ 'This action will delete your current wallet and Secret Recovery Phrase from this device, along with the list of accounts you’ve curated. After resetting with a Secret Recovery Phrase, you’ll see a list of accounts based on the Secret Recovery Phrase you use to reset. This new list will automatically include accounts that have a balance. You’ll also be able to created previously. Custom accounts that you’ve imported will need to be , and any custom tokens you’ve added to an account will need to be as well.',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ getByRole('link', { name: 're-add any other accounts' }),
+ ).toBeInTheDocument();
+ expect(getAllByRole('link', { name: 're-added' })).toHaveLength(2);
+ expect(
+ getByText(
+ 'Make sure you’re using the correct Secret Recovery Phrase before proceeding. You will not be able to undo this.',
+ ),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.js
index 8a5c53ee9..fad8fe801 100644
--- a/ui/pages/onboarding-flow/creation-successful/creation-successful.js
+++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.js
@@ -85,7 +85,7 @@ export default function CreationSuccessful() {
type="link"
rel="noopener noreferrer"
>
- {t('learnMore')}
+ {t('learnMoreUpperCase')}
@@ -103,7 +103,7 @@ export default function CreationSuccessful() {
rounded
onClick={onComplete}
>
- {t('done')}
+ {t('gotIt')}
diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js
index f2277f4e6..2792b2a30 100644
--- a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js
+++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js
@@ -33,10 +33,10 @@ describe('Creation Successful Onboarding View', () => {
.mockReturnValue({ push: pushMock });
});
- it('should call completeOnboarding in the background when "Done" button is clicked', () => {
+ it('should call completeOnboarding in the background when "Got it!" button is clicked', () => {
const { getByText } = renderWithProvider( , store);
- const doneButton = getByText('Done');
- fireEvent.click(doneButton);
+ const gotItButton = getByText('Got it!');
+ fireEvent.click(gotItButton);
expect(completeOnboardingStub).toHaveBeenCalledTimes(1);
});
diff --git a/ui/pages/onboarding-flow/creation-successful/index.scss b/ui/pages/onboarding-flow/creation-successful/index.scss
index 98cf6b80e..54273b1e0 100644
--- a/ui/pages/onboarding-flow/creation-successful/index.scss
+++ b/ui/pages/onboarding-flow/creation-successful/index.scss
@@ -31,8 +31,8 @@
button {
margin-top: 14px;
- max-width: 60%;
- padding: 18px 0;
+ max-width: 280px;
+ padding: 16px 0;
}
}
}
diff --git a/ui/pages/onboarding-flow/import-srp/import-srp.js b/ui/pages/onboarding-flow/import-srp/import-srp.js
index 648a48c9b..b3dc7bbad 100644
--- a/ui/pages/onboarding-flow/import-srp/import-srp.js
+++ b/ui/pages/onboarding-flow/import-srp/import-srp.js
@@ -18,10 +18,11 @@ import {
import { ONBOARDING_CREATE_PASSWORD_ROUTE } from '../../../helpers/constants/routes';
import { clearClipboard } from '../../../helpers/utils/util';
import { useI18nContext } from '../../../hooks/useI18nContext';
+import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
export default function ImportSRP({ submitSecretRecoveryPhrase }) {
const [secretRecoveryPhrase, setSecretRecoveryPhrase] = useState('');
- const [revealSRP, setRevealSRP] = useState(true);
+ const [revealSRP, setRevealSRP] = useState(false);
const [error, setError] = useState('');
const history = useHistory();
const t = useI18nContext();
@@ -56,11 +57,11 @@ export default function ImportSRP({ submitSecretRecoveryPhrase }) {
- {t('learnMore')}
+ {t('learnMoreUpperCase')}
,
])}
diff --git a/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js b/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js
index 558becb5a..96ccc9379 100644
--- a/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js
+++ b/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js
@@ -26,7 +26,7 @@ export default function RecoveryPhraseChips({
const hideSeedPhrase = phraseRevealed === false;
return (
{
@@ -72,7 +72,7 @@ export default function RecoveryPhraseChips({
{word}
diff --git a/ui/pages/onboarding-flow/secure-your-wallet/index.scss b/ui/pages/onboarding-flow/secure-your-wallet/index.scss
index 3a1f80041..c76b01313 100644
--- a/ui/pages/onboarding-flow/secure-your-wallet/index.scss
+++ b/ui/pages/onboarding-flow/secure-your-wallet/index.scss
@@ -8,6 +8,7 @@
&__details {
max-width: 550px;
+ padding-inline-end: 10px;
}
&__actions {
diff --git a/ui/pages/onboarding-flow/secure-your-wallet/secure-your-wallet.js b/ui/pages/onboarding-flow/secure-your-wallet/secure-your-wallet.js
index 4201b7986..1990b7a4c 100644
--- a/ui/pages/onboarding-flow/secure-your-wallet/secure-your-wallet.js
+++ b/ui/pages/onboarding-flow/secure-your-wallet/secure-your-wallet.js
@@ -9,6 +9,7 @@ import {
TYPOGRAPHY,
JUSTIFY_CONTENT,
FONT_WEIGHT,
+ DISPLAY,
} from '../../../helpers/constants/design-system';
import {
ThreeStepProgressBar,
@@ -59,16 +60,13 @@ export default function SecureYourWallet() {
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={4}
+ marginTop={8}
>
{t('seedPhraseIntroTitle')}
-
+
-
-
- {t('seedPhraseIntroSidebarTitleOne')}
-
-
- {t('seedPhraseIntroSidebarCopyOne')}
-
-
-
-
- {t('seedPhraseIntroSidebarTitleTwo')}
-
-
- {t('seedPhraseIntroSidebarBulletOne')}
- {t('seedPhraseIntroSidebarBulletTwo')}
- {t('seedPhraseIntroSidebarBulletThree')}
- {t('seedPhraseIntroSidebarBulletFour')}
-
-
-
-
- {t('seedPhraseIntroSidebarTitleThree')}
-
-
- {t('seedPhraseIntroSidebarCopyTwo')}
-
-
-
-
- {t('seedPhraseIntroSidebarCopyThree')}
-
+
+
+
+ {t('seedPhraseIntroSidebarTitleOne')}
+
+
+ {t('seedPhraseIntroSidebarCopyOne')}
+
+
+
+
+ {t('seedPhraseIntroSidebarTitleTwo')}
+
+
+ {t('seedPhraseIntroSidebarBulletOne')}
+ {t('seedPhraseIntroSidebarBulletThree')}
+ {t('seedPhraseIntroSidebarBulletFour')}
+
+
+
+
+ {t('seedPhraseIntroSidebarTitleThree')}
+
+
+ {t('seedPhraseIntroSidebarCopyTwo')}
+
+
+
+
+ {t('seedPhraseIntroSidebarCopyThree')}
+
+
);
diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss
index 501400efc..a2a6c56c2 100644
--- a/ui/pages/pages.scss
+++ b/ui/pages/pages.scss
@@ -1,4 +1,5 @@
/** Please import your files in alphabetical order **/
+@import 'add-collectible/index';
@import 'import-token/index';
@import 'asset/asset';
@import 'confirm-import-token/index';
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js
index c24ddb868..116effd62 100644
--- a/ui/pages/settings/advanced-tab/advanced-tab.component.js
+++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js
@@ -12,6 +12,10 @@ import Dialog from '../../../components/ui/dialog';
import { getPlatform } from '../../../../app/scripts/lib/util';
import { PLATFORM_FIREFOX } from '../../../../shared/constants/app';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
import {
LEDGER_TRANSPORT_TYPES,
@@ -61,13 +65,22 @@ export default class AdvancedTab extends PureComponent {
showLedgerTransportWarning: false,
};
- showTestNetworksRef = React.createRef();
+ settingsRefs = Array(
+ getSettingsSectionNumber(this.context.t, this.context.t('advanced')),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('advanced'), this.settingsRefs);
+ }
componentDidMount() {
- if (window.location.hash.match(/show-testnets/u)) {
- this.showTestNetworksRef.current.scrollIntoView({ behavior: 'smooth' });
- this.showTestNetworksRef.current.focus();
- }
+ const { t } = this.context;
+ handleSettingsRefs(t, t('advanced'), this.settingsRefs);
}
renderMobileSync() {
@@ -76,6 +89,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -106,6 +120,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -144,6 +159,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -185,6 +201,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -214,6 +231,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -243,7 +261,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -276,6 +294,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -307,6 +326,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -355,6 +375,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -411,6 +432,7 @@ export default class AdvancedTab extends PureComponent {
}
return (
@@ -479,7 +501,7 @@ export default class AdvancedTab extends PureComponent {
: LEDGER_TRANSPORT_NAMES.U2F;
return (
-
+
{t('preferredLedgerConnectionType')}
@@ -578,6 +600,7 @@ export default class AdvancedTab extends PureComponent {
return (
@@ -622,6 +645,7 @@ export default class AdvancedTab extends PureComponent {
return (
diff --git a/ui/pages/settings/alerts-tab/alerts-tab.js b/ui/pages/settings/alerts-tab/alerts-tab.js
index c8d092470..1bbd327e2 100644
--- a/ui/pages/settings/alerts-tab/alerts-tab.js
+++ b/ui/pages/settings/alerts-tab/alerts-tab.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
@@ -8,27 +8,38 @@ import ToggleButton from '../../../components/ui/toggle-button';
import { setAlertEnabledness } from '../../../store/actions';
import { getAlertEnabledness } from '../../../ducks/metamask/metamask';
import { useI18nContext } from '../../../hooks/useI18nContext';
+import { handleHooksSettingsRefs } from '../../../helpers/utils/settings-search';
-const AlertSettingsEntry = ({ alertId, description, title }) => {
+const AlertSettingsEntry = ({ alertId, description, title, alertIndex }) => {
const t = useI18nContext();
+ const settingsRefs = useRef(alertIndex);
+
+ useEffect(() => {
+ handleHooksSettingsRefs(t, t('alerts'), settingsRefs, alertIndex);
+ }, [settingsRefs, t, alertIndex]);
+
const isEnabled = useSelector((state) => getAlertEnabledness(state)[alertId]);
return (
<>
-
{title}
-
-
-
-
setAlertEnabledness(alertId, !isEnabled)}
- value={isEnabled}
- />
+
+
{title}
+
+
+
+
+ setAlertEnabledness(alertId, !isEnabled)}
+ value={isEnabled}
+ />
+
+
>
);
};
@@ -37,6 +48,7 @@ AlertSettingsEntry.propTypes = {
alertId: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
+ alertIndex: PropTypes.number.isRequired,
};
const AlertsTab = () => {
@@ -55,14 +67,17 @@ const AlertsTab = () => {
return (
- {Object.entries(alertConfig).map(([alertId, { title, description }]) => (
-
- ))}
+ {Object.entries(alertConfig).map(
+ ([alertId, { title, description }], index) => (
+
+ ),
+ )}
);
};
diff --git a/ui/pages/settings/alerts-tab/alerts-tab.scss b/ui/pages/settings/alerts-tab/alerts-tab.scss
index de31726bf..f3873b43f 100644
--- a/ui/pages/settings/alerts-tab/alerts-tab.scss
+++ b/ui/pages/settings/alerts-tab/alerts-tab.scss
@@ -6,28 +6,26 @@
grid-template-columns: 8fr 30px max-content;
grid-template-rows: 1fr 1fr;
align-items: center;
+ display: block;
}
- &__body > * {
- border-bottom: 1px solid var(--Grey-100);
- padding: 16px 8px;
- height: 100%;
+ &__description-container {
+ display: flex;
}
- &__body > :first-child {
- padding-left: 32px;
- }
-
- &__body > :nth-child(3n+4) {
- padding-left: 32px;
- }
-
- &__body > :nth-child(3n) {
- padding-right: 32px;
+ &__description-container > * {
+ padding: 0 8px;
}
&__description {
display: flex;
align-items: center;
}
+
+ &__item {
+ border-bottom: 1px solid var(--Grey-100);
+ padding: 16px 32px;
+ display: flex;
+ justify-content: space-between;
+ }
}
diff --git a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js
index 707d4605f..f37f2c962 100644
--- a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js
+++ b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js
@@ -7,6 +7,10 @@ import {
CONTACT_VIEW_ROUTE,
} from '../../../helpers/constants/routes';
import Button from '../../../components/ui/button';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
import EditContact from './edit-contact';
import AddContact from './add-contact';
import ViewContact from './view-contact';
@@ -27,6 +31,24 @@ export default class ContactListTab extends Component {
hideAddressBook: PropTypes.bool,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(this.context.t, this.context.t('contacts')),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('contacts'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('contacts'), this.settingsRefs);
+ }
+
renderAddresses() {
const { addressBook, history, selectedAddress } = this.props;
const contacts = addressBook.filter(({ name }) => Boolean(name));
@@ -124,7 +146,11 @@ export default class ContactListTab extends Component {
const { hideAddressBook } = this.props;
if (!hideAddressBook) {
- return {this.renderAddresses()}
;
+ return (
+
+ {this.renderAddresses()}
+
+ );
}
return null;
}
diff --git a/ui/pages/settings/experimental-tab/experimental-tab.component.js b/ui/pages/settings/experimental-tab/experimental-tab.component.js
index 3d2b16fde..6d74ca459 100644
--- a/ui/pages/settings/experimental-tab/experimental-tab.component.js
+++ b/ui/pages/settings/experimental-tab/experimental-tab.component.js
@@ -1,6 +1,10 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import ToggleButton from '../../../components/ui/toggle-button';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
export default class ExperimentalTab extends PureComponent {
static contextTypes = {
@@ -19,12 +23,30 @@ export default class ExperimentalTab extends PureComponent {
setEIP1559V2Enabled: PropTypes.func,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(this.context.t, this.context.t('experimental')),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('experimental'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('experimental'), this.settingsRefs);
+ }
+
renderTokenDetectionToggle() {
const { t } = this.context;
const { useTokenDetection, setUseTokenDetection } = this.props;
return (
-
+
{t('useTokenDetection')}
@@ -68,7 +90,10 @@ export default class ExperimentalTab extends PureComponent {
} = this.props;
return (
-
+
{t('useCollectibleDetection')}
@@ -114,7 +139,10 @@ export default class ExperimentalTab extends PureComponent {
} = this.props;
return (
-
+
{t('enableOpenSeaAPI')}
diff --git a/ui/pages/settings/flask/view-snap/index.scss b/ui/pages/settings/flask/view-snap/index.scss
index d0efae6f7..4d672ef7a 100644
--- a/ui/pages/settings/flask/view-snap/index.scss
+++ b/ui/pages/settings/flask/view-snap/index.scss
@@ -6,8 +6,6 @@
}
&__subheader {
- @include H4;
-
padding: 16px 4px;
border-bottom: 1px solid var(--alto);
margin-right: 24px;
@@ -51,12 +49,18 @@
}
&__toggle-container {
+ margin-left: auto;
+
@media screen and (max-width: $break-small) {
padding-left: 0;
display: inline-block;
}
}
+ &__toggle-button {
+ margin-right: -12px;
+ }
+
&__content-container {
@media screen and (max-width: $break-small) {
width: 100%;
diff --git a/ui/pages/settings/flask/view-snap/view-snap.js b/ui/pages/settings/flask/view-snap/view-snap.js
index 14c2b050e..5986b7b5b 100644
--- a/ui/pages/settings/flask/view-snap/view-snap.js
+++ b/ui/pages/settings/flask/view-snap/view-snap.js
@@ -86,15 +86,12 @@ function ViewSnap() {
url={authorshipPillUrl}
/>
-
+
@@ -155,8 +152,8 @@ function ViewSnap() {
css={{
maxWidth: '175px',
}}
- onClick={async () => {
- await dispatch(removeSnap(snap));
+ onClick={() => {
+ dispatch(removeSnap(snap));
}}
>
{t('removeSnap')}
diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss
index 315a70456..958915e02 100644
--- a/ui/pages/settings/index.scss
+++ b/ui/pages/settings/index.scss
@@ -13,22 +13,136 @@
flex-flow: column nowrap;
&__header {
- display: flex;
- flex-flow: row nowrap;
- padding: 12px 24px;
- align-items: center;
- flex: 0 0 auto;
+ padding: 8px 24px 8px 24px;
+ position: relative;
- &__title {
- @include H3;
+ @media screen and (max-width: $break-small) {
+ background: var(--ui-1);
+ }
- flex: 1 0 auto;
+ &__title-container {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ flex: 0 0 auto;
+ &__close-button {
+ margin-left: auto;
+ }
+
+ &__close-button::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: var(--ui-4);
+ cursor: pointer;
+ }
+
+ &__title {
+ @include H3;
+
+ flex: 1 0 auto;
+
+ @media screen and (max-width: $break-small) {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 250px;
+ }
+ }
+ }
+
+ &__search {
@media screen and (max-width: $break-small) {
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- max-width: 250px;
+ position: relative;
+ }
+
+ @media screen and (min-width: $break-large) {
+ position: absolute;
+ right: 57px;
+ top: 10px;
+ width: 300px;
+ }
+
+ @media screen and (min-width: $break-midpoint) {
+ width: 400px;
+ }
+
+ &__list {
+ background: var(--ui-white);
+ box-sizing: border-box;
+ box-shadow: 0 0 14px rgba(0, 0, 0, 0.18);
+ border-radius: 6px;
+ position: absolute;
+ width: 100%;
+ z-index: 10;
+
+ > div {
+ &:hover {
+ background: var(--ui-1);
+ }
+ }
+
+ &__item {
+ transition: 200ms ease-in-out;
+ display: grid;
+ align-items: center;
+ padding: 16px;
+ border-top: 1px solid var(--ui-2);
+ cursor: pointer;
+ grid-template-columns: 16px minmax(20px, max-content) 8px auto;
+ gap: 8px;
+
+ &__icon {
+ background: --ui-2;
+ height: 15px;
+ width: 15px;
+ margin-right: 16px;
+ }
+
+ &__request,
+ &__tab,
+ &__section,
+ &__no-matching {
+ @include H6;
+
+ color: var(--ui-4);
+ }
+
+ &__tab-multiple-lines {
+ @media screen and (max-width: $break-small) {
+ width: 90px;
+ }
+ }
+
+ &__section-multiple-lines {
+ @media screen and (max-width: $break-small) {
+ width: 170px;
+ margin-left: 10px;
+ }
+ }
+
+ &__caret {
+ background-image: url('/images/caret-right.svg');
+ width: 8px;
+ height: 10px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ margin: 0 4px;
+
+ [dir='rtl'] & {
+ transform: rotate(180deg);
+ }
+ }
+
+ &__link {
+ @include H6;
+
+ display: inline;
+ color: var(--primary-blue);
+ margin-left: 3px;
+ }
+ }
}
}
}
@@ -110,17 +224,6 @@
}
}
- &__close-button {
- margin-left: auto;
- }
-
- &__close-button::after {
- content: '\00D7';
- font-size: 40px;
- color: var(--ui-4);
- cursor: pointer;
- }
-
&__content {
display: flex;
flex-flow: row nowrap;
diff --git a/ui/pages/settings/info-tab/info-tab.component.js b/ui/pages/settings/info-tab/info-tab.component.js
index 3dd69e4fd..c4de92b31 100644
--- a/ui/pages/settings/info-tab/info-tab.component.js
+++ b/ui/pages/settings/info-tab/info-tab.component.js
@@ -6,6 +6,10 @@ import {
SUPPORT_REQUEST_LINK,
} from '../../../helpers/constants/common';
import { isBeta } from '../../../helpers/utils/build-types';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
export default class InfoTab extends PureComponent {
state = {
@@ -16,13 +20,33 @@ export default class InfoTab extends PureComponent {
t: PropTypes.func,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(this.context.t, this.context.t('about')),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('about'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('about'), this.settingsRefs);
+ }
+
renderInfoLinks() {
const { t } = this.context;
return (
-
{t('links')}
-
+
+ {t('links')}
+
+
-
+
-
+
-
+
-
+
-
+
-
+
{isBeta() ? t('betaMetamaskVersion') : t('metamaskVersion')}
diff --git a/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js b/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
index 378afe584..8cb3ca456 100644
--- a/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
+++ b/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
@@ -14,7 +14,14 @@ import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
import { getProvider } from '../../../../selectors';
-const NetworksListItem = ({ network, networkIsSelected, selectedRpcUrl }) => {
+import { handleHooksSettingsRefs } from '../../../../helpers/utils/settings-search';
+
+const NetworksListItem = ({
+ network,
+ networkIsSelected,
+ selectedRpcUrl,
+ networkIndex,
+}) => {
const t = useI18nContext();
const history = useHistory();
const dispatch = useDispatch();
@@ -37,9 +44,15 @@ const NetworksListItem = ({ network, networkIsSelected, selectedRpcUrl }) => {
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
const displayNetworkListItemAsSelected =
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
+ const settingsRefs = useRef();
+
+ useEffect(() => {
+ handleHooksSettingsRefs(t, t('networks'), settingsRefs, networkIndex);
+ }, [networkIndex, settingsRefs, t]);
return (
{
@@ -76,6 +89,7 @@ NetworksListItem.propTypes = {
network: PropTypes.object.isRequired,
networkIsSelected: PropTypes.bool,
selectedRpcUrl: PropTypes.string,
+ networkIndex: PropTypes.number,
};
export default NetworksListItem;
diff --git a/ui/pages/settings/networks-tab/networks-list/networks-list.js b/ui/pages/settings/networks-tab/networks-list/networks-list.js
index 26be55938..fa91ca798 100644
--- a/ui/pages/settings/networks-tab/networks-list/networks-list.js
+++ b/ui/pages/settings/networks-tab/networks-list/networks-list.js
@@ -16,12 +16,13 @@ const NetworksList = ({
networkIsSelected && !networkDefaultedToProvider,
})}
>
- {networksToRender.map((network) => (
+ {networksToRender.map((network, index) => (
))}
diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js
index 05fcc9e0a..4bc7c25f3 100644
--- a/ui/pages/settings/security-tab/security-tab.component.js
+++ b/ui/pages/settings/security-tab/security-tab.component.js
@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
import ToggleButton from '../../../components/ui/toggle-button';
import { REVEAL_SEED_ROUTE } from '../../../helpers/constants/routes';
import Button from '../../../components/ui/button';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
export default class SecurityTab extends PureComponent {
static contextTypes = {
@@ -21,12 +25,33 @@ export default class SecurityTab extends PureComponent {
usePhishDetect: PropTypes.bool.isRequired,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(
+ this.context.t,
+ this.context.t('securityAndPrivacy'),
+ ),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs);
+ }
+
renderSeedWords() {
const { t } = this.context;
const { history } = this.props;
return (
-
+
{t('revealSeedWords')}
@@ -63,7 +88,7 @@ export default class SecurityTab extends PureComponent {
} = this.props;
return (
-
+
{t('participateInMetaMetrics')}
@@ -92,7 +117,7 @@ export default class SecurityTab extends PureComponent {
} = this.props;
return (
-
+
{t('showIncomingTransactions')}
@@ -120,7 +145,7 @@ export default class SecurityTab extends PureComponent {
const { usePhishDetect, setUsePhishDetect } = this.props;
return (
-
+
{t('usePhishingDetection')}
diff --git a/ui/pages/settings/settings-search-list/index.js b/ui/pages/settings/settings-search-list/index.js
new file mode 100644
index 000000000..5d5fec4be
--- /dev/null
+++ b/ui/pages/settings/settings-search-list/index.js
@@ -0,0 +1,3 @@
+import SettingsSearchList from './settings-search-list';
+
+export default SettingsSearchList;
diff --git a/ui/pages/settings/settings-search-list/settings-search-list.js b/ui/pages/settings/settings-search-list/settings-search-list.js
new file mode 100644
index 000000000..9fc9100d2
--- /dev/null
+++ b/ui/pages/settings/settings-search-list/settings-search-list.js
@@ -0,0 +1,92 @@
+import React, { useContext, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { highlightSearchedText } from '../../../helpers/utils/settings-search';
+import { I18nContext } from '../../../contexts/i18n';
+
+export default function SettingsSearchList({ results, onClickSetting }) {
+ const t = useContext(I18nContext);
+
+ useEffect(highlightSearchedText, [results]);
+
+ return (
+
+ {results.slice(0, 5).map((result) => {
+ const { image, tab, section, id } = result;
+ return (
+ Boolean(image || tab || section) && (
+
+
onClickSetting(result)}
+ >
+
+
+
+
+
+
+
+ )
+ );
+ })}
+ {results.length === 0 && (
+
+
+ {t('settingsSearchMatchingNotFound')}
+
+
+ )}
+
+
+ );
+}
+
+SettingsSearchList.propTypes = {
+ results: PropTypes.array,
+ onClickSetting: PropTypes.func,
+};
diff --git a/ui/pages/settings/settings-search/index.js b/ui/pages/settings/settings-search/index.js
new file mode 100644
index 000000000..302b3e22e
--- /dev/null
+++ b/ui/pages/settings/settings-search/index.js
@@ -0,0 +1,3 @@
+import SettingsSearch from './settings-search';
+
+export default SettingsSearch;
diff --git a/ui/pages/settings/settings-search/settings-search.js b/ui/pages/settings/settings-search/settings-search.js
new file mode 100644
index 000000000..e580d39ca
--- /dev/null
+++ b/ui/pages/settings/settings-search/settings-search.js
@@ -0,0 +1,105 @@
+import React, { useState, useContext } from 'react';
+import PropTypes from 'prop-types';
+import Fuse from 'fuse.js';
+import InputAdornment from '@material-ui/core/InputAdornment';
+import TextField from '../../../components/ui/text-field';
+import { isEqualCaseInsensitive } from '../../../helpers/utils/util';
+import { I18nContext } from '../../../contexts/i18n';
+import SearchIcon from '../../../components/ui/search-icon';
+
+export default function SettingsSearch({
+ onSearch,
+ error,
+ settingsRoutesList,
+}) {
+ const t = useContext(I18nContext);
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchIconColor, setSearchIconColor] = useState('#9b9b9b');
+
+ const settingsRoutesListArray = Object.values(settingsRoutesList);
+ const settingsSearchFuse = new Fuse(settingsRoutesListArray, {
+ shouldSort: true,
+ threshold: 0.2,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: ['tab', 'section', 'description'],
+ });
+
+ // eslint-disable-next-line no-shadow
+ const handleSearch = (searchQuery) => {
+ setSearchQuery(searchQuery);
+ if (searchQuery === '') {
+ setSearchIconColor('#9b9b9b');
+ } else {
+ setSearchIconColor('#24292E');
+ }
+ const fuseSearchResult = settingsSearchFuse.search(searchQuery);
+ const addressSearchResult = settingsRoutesListArray.filter((routes) => {
+ return (
+ routes.tab &&
+ searchQuery &&
+ isEqualCaseInsensitive(routes.tab, searchQuery)
+ );
+ });
+
+ const results = [...addressSearchResult, ...fuseSearchResult];
+ onSearch({ searchQuery, results });
+ };
+
+ const renderStartAdornment = () => {
+ return (
+
+
+
+ );
+ };
+
+ const renderEndAdornment = () => {
+ return (
+ <>
+ {searchQuery && (
+
handleSearch('')}
+ style={{ cursor: 'pointer' }}
+ >
+
+
+ )}
+ >
+ );
+ };
+
+ return (
+
handleSearch(e.target.value)}
+ error={error}
+ fullWidth
+ autoFocus
+ autoComplete="off"
+ style={{ backgroundColor: '#fff' }}
+ startAdornment={renderStartAdornment()}
+ endAdornment={renderEndAdornment()}
+ />
+ );
+}
+
+SettingsSearch.propTypes = {
+ onSearch: PropTypes.func,
+ error: PropTypes.string,
+ settingsRoutesList: PropTypes.array,
+};
diff --git a/ui/pages/settings/settings-search/settings-search.stories.js b/ui/pages/settings/settings-search/settings-search.stories.js
new file mode 100644
index 000000000..cd45c49c1
--- /dev/null
+++ b/ui/pages/settings/settings-search/settings-search.stories.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import SettingsSearch from './settings-search';
+
+export default {
+ title: 'Pages/Settings/SettingsSearch',
+ id: __filename,
+};
+
+export const SettingsSearchComponent = () => {
+ return ;
+};
diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js
index 0836d428b..5dca12135 100644
--- a/ui/pages/settings/settings-tab/settings-tab.component.js
+++ b/ui/pages/settings/settings-tab/settings-tab.component.js
@@ -10,6 +10,11 @@ import Jazzicon from '../../../components/ui/jazzicon';
import BlockieIdenticon from '../../../components/ui/identicon/blockieIdenticon';
import Typography from '../../../components/ui/typography';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
+
const sortedCurrencies = availableCurrencies.sort((a, b) => {
return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase());
});
@@ -53,6 +58,24 @@ export default class SettingsTab extends PureComponent {
tokenList: PropTypes.object,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(this.context.t, this.context.t('general')),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('general'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('general'), this.settingsRefs);
+ }
+
renderCurrentConversion() {
const { t } = this.context;
const {
@@ -62,7 +85,7 @@ export default class SettingsTab extends PureComponent {
} = this.props;
return (
-
+
{t('currencyConversion')}
@@ -96,7 +119,7 @@ export default class SettingsTab extends PureComponent {
const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : '';
return (
-
+
{t('currentLanguage')}
@@ -124,7 +147,11 @@ export default class SettingsTab extends PureComponent {
const { hideZeroBalanceTokens, setHideZeroBalanceTokens } = this.props;
return (
-
+
{t('hideZeroBalanceTokens')}
@@ -160,7 +187,11 @@ export default class SettingsTab extends PureComponent {
});
return (
-
+
{t('accountIdenticon')}
@@ -238,7 +269,7 @@ export default class SettingsTab extends PureComponent {
} = this.props;
return (
-
+
{t('primaryCurrencySetting')}
diff --git a/ui/pages/settings/settings-tab/settings-tab.container.test.js b/ui/pages/settings/settings-tab/settings-tab.component.test.js
similarity index 100%
rename from ui/pages/settings/settings-tab/settings-tab.container.test.js
rename to ui/pages/settings/settings-tab/settings-tab.component.test.js
diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js
index 7d6f37458..bc4fc2ac2 100644
--- a/ui/pages/settings/settings.component.js
+++ b/ui/pages/settings/settings.component.js
@@ -22,6 +22,8 @@ import {
EXPERIMENTAL_ROUTE,
ADD_NETWORK_ROUTE,
} from '../../helpers/constants/routes';
+import { getSettingsRoutes } from '../../helpers/utils/settings-search';
+
import SettingsTab from './settings-tab';
import AlertsTab from './alerts-tab';
import NetworksTab from './networks-tab';
@@ -34,6 +36,8 @@ import ExperimentalTab from './experimental-tab';
import SnapListTab from './flask/snaps-list-tab';
import ViewSnap from './flask/view-snap';
///: END:ONLY_INCLUDE_IN
+import SettingsSearch from './settings-search';
+import SettingsSearchList from './settings-search-list';
class SettingsPage extends PureComponent {
static propTypes = {
@@ -59,6 +63,9 @@ class SettingsPage extends PureComponent {
state = {
lastFetchedConversionDate: null,
+ searchResults: [],
+ isSearchList: false,
+ searchText: '',
};
componentDidMount() {
@@ -76,6 +83,15 @@ class SettingsPage extends PureComponent {
}
}
+ handleClickSetting(setting) {
+ const { history } = this.props;
+ history.push(setting.route);
+ this.setState({
+ searchResults: '',
+ isSearchList: '',
+ });
+ }
+
render() {
const {
history,
@@ -85,6 +101,10 @@ class SettingsPage extends PureComponent {
addNewNetwork,
isSnapViewPage,
} = this.props;
+
+ const { searchResults, isSearchList, searchText } = this.state;
+ const { t } = this.context;
+
return (
history.push(backRoute)}
/>
)}
- {this.renderTitle()}
-
{
- if (addNewNetwork) {
- history.push(NETWORKS_ROUTE);
- } else {
- history.push(mostRecentOverviewPage);
- }
- }}
- />
+
+ {this.renderTitle()}
+
+
{
+ if (addNewNetwork) {
+ history.push(NETWORKS_ROUTE);
+ } else {
+ history.push(mostRecentOverviewPage);
+ }
+ }}
+ />
+
+
+
+ {
+ this.setState({
+ searchResults: results,
+ isSearchList: searchQuery !== '',
+ searchText: searchQuery,
+ });
+ }}
+ settingsRoutesList={getSettingsRoutes(t)}
+ />
+ {isSearchList && searchText.length >= 3 && (
+ this.handleClickSetting(setting)}
+ />
+ )}
+
+
{this.renderTabs()}
@@ -142,7 +186,11 @@ class SettingsPage extends PureComponent {
titleText = t('settings');
}
- return
{titleText}
;
+ return (
+
+ {titleText}
+
+ );
}
renderSubHeader() {
diff --git a/ui/pages/settings/settings.component.test.js b/ui/pages/settings/settings.component.test.js
new file mode 100644
index 000000000..f2b8c6f37
--- /dev/null
+++ b/ui/pages/settings/settings.component.test.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import TextField from '../../components/ui/text-field';
+import Settings from './settings.container';
+import SettingsSearch from './settings-search';
+
+describe('SettingsPage', () => {
+ let wrapper;
+
+ const props = {
+ isAddressEntryPage: false,
+ backRoute: '/',
+ currentPath: '/settings',
+ location: '/settings',
+ mostRecentOverviewPage: '',
+ isPopup: false,
+ pathnameI18nKey: undefined,
+ addressName: '',
+ initialBreadCrumbRoute: undefined,
+ initialBreadCrumbKey: undefined,
+ addNewNetwork: false,
+ conversionDate: Date.now(),
+ };
+
+ beforeEach(() => {
+ wrapper = shallow(
, {
+ context: {
+ t: (str) => str,
+ },
+ });
+ });
+
+ it('should render title correctly', () => {
+ expect(
+ wrapper.find('.settings-page__header__title-container__title').text(),
+ ).toStrictEqual('settings');
+ });
+
+ it('should render search correctly', () => {
+ wrapper = shallow(
+
undefined} settingsRoutesList={[]} />,
+ {
+ context: {
+ t: (s) => `${s}`,
+ },
+ },
+ );
+
+ expect(wrapper.find(TextField).props().id).toStrictEqual('search-settings');
+ expect(wrapper.find(TextField).props().value).toStrictEqual('');
+ });
+});
diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js
index d4eccac17..3cb8dbe1d 100644
--- a/ui/pages/swaps/build-quote/build-quote.js
+++ b/ui/pages/swaps/build-quote/build-quote.js
@@ -595,23 +595,32 @@ export default function BuildQuote({
flexDirection={FLEX_DIRECTION.COLUMN}
>
-
+
{t('stxDescription')}
{t('stxBenefit1')}
{t('stxBenefit2')}
{t('stxBenefit3')}
- {t('stxBenefit4')}
+
+ {t('stxBenefit4')}
+
+ {' *'}
+
+
{result.notImported && (
-
+
{t('import')}
)}
diff --git a/ui/pages/token-details/token-details-page.js b/ui/pages/token-details/token-details-page.js
index a26ada620..7441e1994 100644
--- a/ui/pages/token-details/token-details-page.js
+++ b/ui/pages/token-details/token-details-page.js
@@ -93,7 +93,7 @@ export default function TokenDetailsPage() {
color={COLORS.BLACK}
className="token-details__token-value"
>
- {tokenBalance}
+ {tokenBalance || ''}
{this.renderSubmitButton()}
- {t('importAccountText', [
- onRestore()}
- >
- {t('importAccountLinkText')}
- ,
- ])}
+ onRestore()}
+ >
+ {t('forgotPassword')}
+
{t('needHelp', [
diff --git a/ui/pages/unlock-page/unlock-page.component.test.js b/ui/pages/unlock-page/unlock-page.component.test.js
index 5029f6a0e..40a3d265b 100644
--- a/ui/pages/unlock-page/unlock-page.component.test.js
+++ b/ui/pages/unlock-page/unlock-page.component.test.js
@@ -23,7 +23,7 @@ describe('Unlock Page Component', () => {
configureMockStore()({ metamask: { currentLocale: 'en' } }),
);
- fireEvent.click(getByText('import using Secret Recovery Phrase'));
+ fireEvent.click(getByText('Forgot password?'));
expect(props.onRestore.calledOnce).toStrictEqual(true);
});
});
diff --git a/ui/store/actionConstants.js b/ui/store/actionConstants.js
index 0bc5b5c80..1e426daea 100644
--- a/ui/store/actionConstants.js
+++ b/ui/store/actionConstants.js
@@ -113,4 +113,4 @@ export const SET_SMART_TRANSACTIONS_ERROR = 'SET_SMART_TRANSACTIONS_ERROR';
export const DISMISS_SMART_TRANSACTIONS_ERROR_MESSAGE =
'DISMISS_SMART_TRANSACTIONS_ERROR_MESSAGE';
-export const SET_CURRENCY_INPUT_SWITCH = 'SET_CURRENCY_INPUT_SWITCH';
+export const TOGGLE_CURRENCY_INPUT_SWITCH = 'TOGGLE_CURRENCY_INPUT_SWITCH';
diff --git a/yarn.lock b/yarn.lock
index 561e6bf79..b973c6919 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2707,6 +2707,11 @@
web3 "^0.20.7"
web3-provider-engine "^16.0.3"
+"@metamask/design-tokens@^1.3.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-1.4.0.tgz#54e3924bc354f78d7a90ecac90da747ac60a01a1"
+ integrity sha512-pQD0x8gLBk1K558PJCjR0VP3YZ+mPKmczBqFGt+/OY/rLyvN998if6Gm7IuqMviH4am4M+fJ/rngVfF/1EJUHg==
+
"@metamask/eslint-config-jest@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@metamask/eslint-config-jest/-/eslint-config-jest-9.0.0.tgz#516fdf1f03f6f006b26ca790bf748e2189d19d17"
@@ -4086,6 +4091,13 @@
"@babel/runtime" "^7.10.3"
"@testing-library/dom" "^7.17.1"
+"@testing-library/user-event@^14.0.0-beta.12":
+ version "14.0.0-beta.12"
+ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.0.0-beta.12.tgz#8df662578e49371fd80d5923d92f3c378f7dd927"
+ integrity sha512-vFZQBBzO14bJseKAS78Ae/coSuJbgrs7ywRZw88Hc52Le8RJGehdxR4w25Oj7QVNpZZpz0R6q1zMVdYGtPbd2A==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -11405,10 +11417,10 @@ ethers@^4.0.20, ethers@^4.0.28:
uuid "2.0.1"
xmlhttprequest "1.8.0"
-ethers@^5.0.8, ethers@^5.4.0, ethers@^5.4.1, ethers@^5.4.5:
- version "5.5.1"
- resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.1.tgz#d3259a95a42557844aa543906c537106c0406fbf"
- integrity sha512-RodEvUFZI+EmFcE6bwkuJqpCYHazdzeR1nMzg+YWQSmQEsNtfl1KHGfp/FWZYl48bI/g7cgBeP2IlPthjiVngw==
+ethers@^5.0.8, ethers@^5.4.0, ethers@^5.4.1, ethers@^5.4.5, ethers@^5.5.1:
+ version "5.5.4"
+ resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.4.tgz#e1155b73376a2f5da448e4a33351b57a885f4352"
+ integrity sha512-N9IAXsF8iKhgHIC6pquzRgPBJEzc9auw3JoRkaKe+y4Wl/LFBtDDunNe7YmdomontECAcC5APaAgWZBiu1kirw==
dependencies:
"@ethersproject/abi" "5.5.0"
"@ethersproject/abstract-provider" "5.5.1"
@@ -12028,7 +12040,7 @@ fast-deep-equal@^2.0.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
-fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+fast-deep-equal@^3.0.0, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -13456,6 +13468,18 @@ graphql-request@^1.8.2:
dependencies:
cross-fetch "2.2.2"
+graphql-subscriptions@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d"
+ integrity sha512-95yD/tKi24q8xYa7Q9rhQN16AYj5wPbrb8tmHGM3WRc9EBmWrG/0kkMl+tQG8wcEuE9ibR4zyOM31p5Sdr2v4g==
+ dependencies:
+ iterall "^1.3.0"
+
+"graphql@^14.0.2 || ^15.5":
+ version "15.8.0"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38"
+ integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==
+
gridplus-sdk@^0.9.7:
version "0.9.10"
resolved "https://registry.yarnpkg.com/gridplus-sdk/-/gridplus-sdk-0.9.10.tgz#8835e8bc9a2ca7ae163520ddcc0b313350e67dd2"
@@ -25231,6 +25255,14 @@ storybook-addon-outline@^1.4.1:
"@storybook/core-events" "^6.3.0"
ts-dedent "^2.1.1"
+storybook-dark-mode@^1.0.9:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/storybook-dark-mode/-/storybook-dark-mode-1.0.9.tgz#40c15aa340bc700df2fb4f1345250e6fdace0b3a"
+ integrity sha512-ITPXM2OSaga1zM5blpZy5HxMWAhrAqYi9aJtLgRtSdgoRrxVNAInDRD14TjmObdgLHNWxINoNbnEB+sKETa+iw==
+ dependencies:
+ fast-deep-equal "^3.0.0"
+ memoizerific "^1.11.3"
+
stream-browserify@^2.0.0, stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"