diff --git a/.circleci/config.yml b/.circleci/config.yml index 76992feca..bf6513533 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,9 +56,6 @@ workflows: - prep-build-test-flask: requires: - prep-deps - - prep-build-test-metrics: - requires: - - prep-deps - test-storybook: requires: - prep-deps @@ -87,12 +84,6 @@ workflows: - test-e2e-firefox-snaps: requires: - prep-build-test-flask - - test-e2e-chrome-metrics: - requires: - - prep-build-test-metrics - - test-e2e-firefox-metrics: - requires: - - prep-build-test-metrics - test-unit: requires: - prep-deps @@ -137,8 +128,6 @@ workflows: - test-mozilla-lint-flask - test-e2e-chrome - test-e2e-firefox - - test-e2e-chrome-metrics - - test-e2e-firefox-metrics - test-e2e-chrome-snaps - test-e2e-firefox-snaps - benchmark: @@ -338,27 +327,6 @@ jobs: - dist-test - builds-test - prep-build-test-metrics: - executor: node-browsers-medium-plus - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Build extension for testing metrics - command: yarn build:test:metrics - - run: - name: Move test build to 'dist-test-metrics' to avoid conflict with production build - command: mv ./dist ./dist-test-metrics - - run: - name: Move test zips to 'builds-test' to avoid conflict with production build - command: mv ./builds ./builds-test-metrics - - persist_to_workspace: - root: . - paths: - - dist-test-metrics - - builds-test-metrics - prep-build-storybook: executor: node-browsers steps: @@ -553,33 +521,6 @@ jobs: path: test-artifacts destination: test-artifacts - test-e2e-chrome-metrics: - executor: node-browsers - steps: - - checkout - - run: - name: Re-Install Chrome - command: ./.circleci/scripts/chrome-install.sh - - attach_workspace: - at: . - - run: - name: Move test build to dist - command: mv ./dist-test-metrics ./dist - - run: - name: Move test zips to builds - command: mv ./builds-test-metrics ./builds - - run: - name: test:e2e:chrome:metrics - command: | - if .circleci/scripts/test-run-e2e.sh - then - yarn test:e2e:chrome:metrics --retries 2 - fi - no_output_timeout: 20m - - store_artifacts: - path: test-artifacts - destination: test-artifacts - test-e2e-firefox: executor: node-browsers-medium-plus steps: @@ -607,33 +548,6 @@ jobs: path: test-artifacts destination: test-artifacts - test-e2e-firefox-metrics: - executor: node-browsers - steps: - - checkout - - run: - name: Install Firefox - command: ./.circleci/scripts/firefox-install.sh - - attach_workspace: - at: . - - run: - name: Move test build to dist - command: mv ./dist-test-metrics ./dist - - run: - name: Move test zips to builds - command: mv ./builds-test-metrics ./builds - - run: - name: test:e2e:firefox:metrics - command: | - if .circleci/scripts/test-run-e2e.sh - then - yarn test:e2e:firefox:metrics --retries 2 - fi - no_output_timeout: 20m - - store_artifacts: - path: test-artifacts - destination: test-artifacts - benchmark: executor: node-browsers-medium-plus steps: 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/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4cd184317..da7708be2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,47 @@ -Fixes: # +## Explanation -Explanation: + + +## More information + + + +## Screenshots/Screencaps + + + +### Before + + + +### After + + + +## Manual testing steps + + 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/metamask-storybook-theme.js b/.storybook/metamask-storybook-theme.js index 6623f4fe0..49f8c814d 100644 --- a/.storybook/metamask-storybook-theme.js +++ b/.storybook/metamask-storybook-theme.js @@ -1,12 +1,10 @@ // .storybook/YourTheme.js import { create } from '@storybook/theming'; -import logo from '../app/images/logo/metamask-logo-horizontal.svg'; export default create({ base: 'light', brandTitle: 'MetaMask Storybook', - brandImage: logo, // Typography fontBase: 'Euclid, Roboto, Helvetica, Arial, sans-serif', 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/CHANGELOG.md b/CHANGELOG.md index e0b258263..2586fdfc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.12.0] +### Added +- Add a search feature to the settings page ([#13214](https://github.com/MetaMask/metamask-extension/pull/13214)) +- Add AirGap Vault detail links to the hardware wallet connection flow ([#13650](https://github.com/MetaMask/metamask-extension/pull/13650)) + +### Changed +- Prevent users from entering too long a number for slippage in swaps ([#13914](https://github.com/MetaMask/metamask-extension/pull/13914)) +- Hide non-essential information in our EIP-1559 v2 gas modal when the gas api is down ([#13865](https://github.com/MetaMask/metamask-extension/pull/13865)) +- Updating colors of the account list ([#13864](https://github.com/MetaMask/metamask-extension/pull/13864)) +- Show a more useful warning is users don't have enough of their networks base currency to pay for gas ([#13182](https://github.com/MetaMask/metamask-extension/pull/13182)) +- Update "Forgot Password?" copy ([#13493](https://github.com/MetaMask/metamask-extension/pull/13493)) +- Show the address of the contract that is being interacted with next to the method name in transaction confirmation headers ([#13683](https://github.com/MetaMask/metamask-extension/pull/13683)) +- Show the address of the contract that is being interacted next to 'Transfer' and 'Transfer From' method names in transaction confirmation headers ([#13776](https://github.com/MetaMask/metamask-extension/pull/13776)) +- Performance and UX improvements for Gridplus lattice users (([#14158]https://github.com/MetaMask/metamask-extension/pull/14158)) + +### Fixed +- Ensure long signature request text is visible ([#13828](https://github.com/MetaMask/metamask-extension/pull/13828)) +- Fix spelling of 'Ethereum' in German translation ([#13915](https://github.com/MetaMask/metamask-extension/pull/13915)) +- Fix cases where the action buttons in a switch network confirmation window wouldn't work ([#13847](https://github.com/MetaMask/metamask-extension/pull/13847)) +- Ensure the origin of a site requesting permissions is fully visible in the permission request UI ([#13868](https://github.com/MetaMask/metamask-extension/pull/13868)) +- Fix visual overflow problems with the account list in the connect flow + - ([#13859](https://github.com/MetaMask/metamask-extension/pull/13859)) + - ([#13592](https://github.com/MetaMask/metamask-extension/pull/13592)) +- Show the users primary currency in the "Max Base Fee" and "Priority Fee" fields of the gas customization window ([#13830](https://github.com/MetaMask/metamask-extension/pull/13830)) +- Ensure latest gas estimates are shown on the transaction screen for users of the EIP-1559 v2 gas UI ([#13809](https://github.com/MetaMask/metamask-extension/pull/13809)) +- Fix to allow toggling of the currency in the send flow when the user has "fiat" selected as the primary currency ([#13813](https://github.com/MetaMask/metamask-extension/pull/13813)) +- Shows the sign and cancel button fully in signature page ([#13686](https://github.com/MetaMask/metamask-extension/pull/13686)) +- Harden keyring type check in EthOverview ([#13711](https://github.com/MetaMask/metamask-extension/pull/13711)) +- Update "Forgot Password?" copy ([#13493](https://github.com/MetaMask/metamask-extension/pull/13493)) +- Confirm transaction page: use method name only for contract transactions ([#13643](https://github.com/MetaMask/metamask-extension/pull/13643)) +- Fix issues +- **[FLASK]** Fix Snap permission list item shrinkage with short permission names ([#13996](https://github.com/MetaMask/metamask-extension/pull/13996)) + ## [10.11.4] ### Added - **[FLASK]** Snap removal confirmation ([#13619](https://github.com/MetaMask/metamask-extension/pull/13619)) @@ -38,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix bug that users who are connected to another extension would hit when viewing connected sites ([#13974](https://github.com/MetaMask/metamask-extension/pull/13974)) + ## [10.11.1] ### Changed - Fixes GridPlus Lattice bugs by upgrading to `gridplus-sdk` v1.0.0, `eth-lattice-keyring` v0.5.0 and to compatibility with v0.14.0 ([#13834](https://github.com/MetaMask/metamask-extension/pull/13834)) @@ -2807,7 +2841,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.11.4...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.12.0...HEAD +[10.12.0]: https://github.com/MetaMask/metamask-extension/compare/v10.11.4...v10.12.0 [10.11.4]: https://github.com/MetaMask/metamask-extension/compare/v10.11.3...v10.11.4 [10.11.3]: https://github.com/MetaMask/metamask-extension/compare/v10.11.2...v10.11.3 [10.11.2]: https://github.com/MetaMask/metamask-extension/compare/v10.11.1...v10.11.2 diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index eede6a17e..998726fa6 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 0aa9684b7..925a51c0a 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 69d60edd9..ce6c09dd5 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 fe484e6de..e9a0a85fc 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 9585515d2..2d0ab7177 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 290993179..0067c616c 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 edab4e7a4..35b7c39f5 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" @@ -1532,7 +1525,7 @@ "message": "niedrig" }, "mainnet": { - "message": "Athereum Hauptnetz" + "message": "Ethereum Hauptnetz" }, "makeAnotherSwap": { "message": "Eine neue Wallet erstellen" @@ -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 0932794cb..0de718e3c 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 1b537468e..d74f6cca4 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" @@ -58,6 +58,10 @@ "message": "$1 may access and spend up to this max amount", "description": "$1 is the url of the site requesting ability to spend" }, + "accessAndSpendNoticeNFT": { + "message": "$1 may access and spend this asset", + "description": "$1 is the url of the site requesting ability to spend" + }, "accessingYourCamera": { "message": "Accessing your camera..." }, @@ -98,6 +102,9 @@ "addANetwork": { "message": "Add a network" }, + "addANetworkManually": { + "message": "Add a network manually" + }, "addANickname": { "message": "Add a nickname" }, @@ -137,6 +144,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 +201,12 @@ "aggregatorFeeCost": { "message": "Aggregator network fee" }, + "airgapVault": { + "message": "AirGap Vault" + }, + "airgapVaultTutorial": { + "message": " (Tutorials)" + }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, @@ -267,6 +283,9 @@ "approvedAmountWithColon": { "message": "Approved amount:" }, + "approvedAsset": { + "message": "Approved asset" + }, "areYouDeveloper": { "message": "Are you a developer?" }, @@ -397,8 +416,15 @@ "description": "$1 represents the cypto symbol to be purchased" }, "buyCryptoWithTransakDescription": { - "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" + "message": "Transak supports credit & debit cards, Apple Pay, MobiKwik, and bank transfers (depending on location) in 100+ countries. $1 deposits directly into your MetaMask account.", + "description": "$1 represents the crypto 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 +493,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" @@ -612,6 +641,9 @@ "convertTokenToNFTDescription": { "message": "We've detected that this asset is an NFT. Metamask now has full native support for NFTs. Would you like to remove it from your token list and add it as an NFT?" }, + "convertTokenToNFTExistDescription": { + "message": "We’ve detected that this asset has been added as an NFT. Would you like to remove it from your token list?" + }, "copiedExclamation": { "message": "Copied!" }, @@ -691,6 +723,9 @@ "customGasSubTitle": { "message": "Increasing fee may decrease processing times, but it is not guaranteed." }, + "customNetworks": { + "message": "Custom networks" + }, "customSpendLimit": { "message": "Custom Spend Limit" }, @@ -714,6 +749,9 @@ "message": "$1 has recommended this price.", "description": "$1 represents the Dapp's origin" }, + "darkTheme": { + "message": "Dark" + }, "data": { "message": "Data" }, @@ -749,6 +787,9 @@ "decryptRequest": { "message": "Decrypt request" }, + "defaultTheme": { + "message": "Default" + }, "delete": { "message": "Delete" }, @@ -814,6 +855,9 @@ "dontShowThisAgain": { "message": "Don't show this again" }, + "downArrow": { + "message": "down arrow" + }, "downloadGoogleChrome": { "message": "Download Google Chrome" }, @@ -989,7 +1033,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 +1250,9 @@ "forgetDevice": { "message": "Forget this device" }, + "forgotPassword": { + "message": "Forgot password?" + }, "from": { "message": "From" }, @@ -1334,6 +1381,9 @@ "goerli": { "message": "Goerli Test Network" }, + "gotIt": { + "message": "Got it!" + }, "grantedToWithColon": { "message": "Granted to:" }, @@ -1372,6 +1422,9 @@ "hide": { "message": "Hide" }, + "hideSeedPhrase": { + "message": "Hide seed phrase" + }, "hideToken": { "message": "Hide token" }, @@ -1408,19 +1461,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 +1524,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." }, @@ -1647,6 +1697,9 @@ "letsGoSetUp": { "message": "Yes, let’s get set up!" }, + "levelArrow": { + "message": "level arrow" + }, "likeToImportTokens": { "message": "Would you like to import these tokens?" }, @@ -1677,6 +1730,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 +1872,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 +1989,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!" }, @@ -1948,7 +2008,7 @@ "message": "“$1” was successfully added!" }, "newPassword": { - "message": "New password (min 8 chars)" + "message": "New password (8 characters min)" }, "newToMetaMask": { "message": "New to MetaMask?" @@ -2199,6 +2259,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 +2278,9 @@ "or": { "message": "or" }, + "orDeposit": { + "message": "or deposit from another account." + }, "origin": { "message": "Origin" }, @@ -2236,11 +2302,18 @@ "passwordSetupDetails": { "message": "This password will unlock your MetaMask wallet only on this device. MetaMask can not recover this password." }, + "passwordStrength": { + "message": "Password strength: $1", + "description": "Return password strength to the user when user wants to create password." + }, + "passwordStrengthDescription": { + "message": "A strong password can improve the security of your wallet should your device be stolen or compromised." + }, "passwordTermsWarning": { "message": "I understand that MetaMask cannot recover this password for me. $1" }, "passwordsDontMatch": { - "message": "Passwords Don't Match" + "message": "Passwords don't match" }, "pastePrivateKey": { "message": "Enter your private key string here:", @@ -2359,6 +2432,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." }, @@ -2462,12 +2541,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" @@ -2490,6 +2578,9 @@ "revealSeedWordsWarningTitle": { "message": "DO NOT share this phrase with anyone!" }, + "revealTheSeedPhrase": { + "message": "Reveal seed phrase" + }, "rinkeby": { "message": "Rinkeby Test Network" }, @@ -2523,6 +2614,9 @@ "searchResults": { "message": "Search Results" }, + "searchSettings": { + "message": "Search in settings" + }, "searchTokens": { "message": "Search Tokens" }, @@ -2535,9 +2629,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" }, @@ -2560,22 +2651,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!" @@ -2669,6 +2760,9 @@ "settings": { "message": "Settings" }, + "settingsSearchMatchingNotFound": { + "message": "No matching results found" + }, "shorthandVersion": { "message": "v$1", "description": "$1 is replaced by a version string (e.g. 1.2.3)" @@ -2755,7 +2849,7 @@ "message": "Slow" }, "smartTransaction": { - "message": "Smart transaction" + "message": "Smart Transaction" }, "snapAccess": { "message": "$1 snap has access to:", @@ -2915,20 +3009,23 @@ "storePhrase": { "message": "Store this phrase in a password manager like 1Password." }, + "strong": { + "message": "Strong" + }, "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" @@ -2940,7 +3037,13 @@ "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:" + }, + "stxErrorNotEnoughFunds": { + "message": "Not enough funds for a smart transaction." + }, + "stxErrorUnavailable": { + "message": "Smart Transactions are temporarily unavailable." }, "stxFailure": { "message": "Swap failed" @@ -2949,8 +3052,11 @@ "message": "Sudden market changes can cause failures. If the problem persists, please reach out to $1.", "description": "This message is shown to a user if their swap fails. The $1 will be replaced by support.metamask.io" }, - "stxFallbackToNormal": { - "message": "You can still swap using the normal method or wait for cheaper gas fees and less failures with smart transactions." + "stxFallbackPendingTx": { + "message": "Smart Transactions are temporarily unavailable because you have a pending transaction." + }, + "stxFallbackUnavailable": { + "message": "You can still swap your tokens even while Smart Transactions are unavailable." }, "stxPendingFinalizing": { "message": "Finalizing..." @@ -2962,7 +3068,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!" @@ -2977,8 +3083,11 @@ "stxTryRegular": { "message": "Try a regular swap." }, + "stxTryingToCancel": { + "message": "Trying to cancel your transaction..." + }, "stxUnavailable": { - "message": "Smart transactions temporarily unavailable" + "message": "Smart Transactions are disabled" }, "stxUnknown": { "message": "Status unknown" @@ -3393,6 +3502,12 @@ "testFaucet": { "message": "Test Faucet" }, + "theme": { + "message": "Theme" + }, + "themeDescription": { + "message": "Choose your preferred MetaMask theme." + }, "thisWillCreate": { "message": "This will create a new wallet and Secret Recovery Phrase" }, @@ -3632,6 +3747,12 @@ "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." + }, + "upArrow": { + "message": "up arrow" + }, "updatedWithDate": { "message": "Updated $1" }, @@ -3659,6 +3780,9 @@ "useTokenDetectionDescription": { "message": "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." }, + "useTokenDetectionPrivacyDesc": { + "message": "Automatically displaying tokens sent to your account involves communication with third party servers to fetch token’s images. Those serves will have access to your IP address." + }, "usedByClients": { "message": "Used by a variety of different clients" }, @@ -3736,6 +3860,9 @@ "walletCreationSuccessTitle": { "message": "Wallet creation successful" }, + "weak": { + "message": "Weak" + }, "web3ShimUsageNotification": { "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", "description": "$1 is a clickable link." diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 1e08c80b1..0ce491da5 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 99a8cd142..bb9858c3f 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 4fb324dc2..b6f929aab 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 a5a46a0cc..c4cc6a32c 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 54932cba4..3301f21dd 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 e82df7c93..474dcc1b0 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 99168b468..e5cd939f9 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 7e7ca2db5..36beece50 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 db88253d9..8b8531835 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 1c50602b4..c466cb53c 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 ce6a13139..f96e8b911 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 1a15f8110..a66e1d4d6 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 8a06c463e..058c26f89 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 4512be6fe..b59af3082 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 79bcad27a..1c575b4a9 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 ccaaec61b..c000290ed 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 9d8453bc8..240a7c198 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 14c0dfe4d..e41892d56 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 7c9e3b8bc..b62072110 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 3d9489ad4..2f4f94f53 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 41c685962..be9cafadc 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 1c174945b..8d5e1019e 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 afbeb96b5..8b161b3cf 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 fe5d4482c..06cf6f692 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 6e839d17b..49d76144d 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 60cd3a48e..0fd6d8bb5 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 046dc927c..4ebce0449 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 0a49cbb19..f37701547 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 3e62b14ab..447dcde42 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 a2634a088..dc1a61406 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 ace2ed1ee..b9f400d7e 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 263ca97dc..9cd734cdc 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 29d9ce0b3..403cafd28 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 cc8ee51f9..de29ab1d5 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 9035fbaa7..943a36afd 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 7cabd6945..b16184e25 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/build-types/beta/images/logo/metamask-logo-horizontal-dark.svg b/app/build-types/beta/images/logo/metamask-logo-horizontal-dark.svg deleted file mode 100644 index 43a44eb0e..000000000 --- a/app/build-types/beta/images/logo/metamask-logo-horizontal-dark.svg +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/build-types/beta/images/logo/metamask-logo-horizontal.svg b/app/build-types/beta/images/logo/metamask-logo-horizontal.svg deleted file mode 100644 index 3155a5149..000000000 --- a/app/build-types/beta/images/logo/metamask-logo-horizontal.svg +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/build-types/flask/images/logo/metamask-logo-horizontal-dark.svg b/app/build-types/flask/images/logo/metamask-logo-horizontal-dark.svg deleted file mode 100644 index 450ac8434..000000000 --- a/app/build-types/flask/images/logo/metamask-logo-horizontal-dark.svg +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/build-types/flask/images/logo/metamask-logo-horizontal.svg b/app/build-types/flask/images/logo/metamask-logo-horizontal.svg deleted file mode 100644 index c38ba8c45..000000000 --- a/app/build-types/flask/images/logo/metamask-logo-horizontal.svg +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/caret-left-black.svg b/app/images/caret-left-black.svg deleted file mode 100644 index f225424f0..000000000 --- a/app/images/caret-left-black.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/caret-left.svg b/app/images/caret-left.svg deleted file mode 100644 index 775d76e39..000000000 --- a/app/images/caret-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/caret-right.svg b/app/images/caret-right.svg deleted file mode 100644 index 1308f59ce..000000000 --- a/app/images/caret-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/icons/caret-down.svg b/app/images/icons/caret-down.svg deleted file mode 100644 index 8ab7b0319..000000000 --- a/app/images/icons/caret-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/images/icons/collapse.svg b/app/images/icons/collapse.svg deleted file mode 100644 index 74d6207c9..000000000 --- a/app/images/icons/collapse.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/images/logo/metamask-logo-horizontal.svg b/app/images/logo/metamask-logo-horizontal.svg deleted file mode 100644 index bc60a0782..000000000 --- a/app/images/logo/metamask-logo-horizontal.svg +++ /dev/null @@ -1 +0,0 @@ - \ 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/background.js b/app/scripts/background.js index 0c941c7f9..4e6176436 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -16,6 +16,7 @@ import { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, + PLATFORM_FIREFOX, } from '../../shared/constants/app'; import { SECOND } from '../../shared/constants/time'; import { @@ -38,6 +39,7 @@ import rawFirstTimeState from './first-time-state'; import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code'; import getObjStructure from './lib/getObjStructure'; import setupEnsIpfsResolver from './lib/ens-ipfs/setup'; +import { getPlatform } from './lib/util'; /* eslint-enable import/first */ const { sentry } = global; @@ -345,12 +347,22 @@ function setupController(initState, initLangCode) { */ function connectRemote(remotePort) { const processName = remotePort.name; - const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]; if (metamaskBlockedPorts.includes(remotePort.name)) { return; } + let isMetaMaskInternalProcess = false; + const sourcePlatform = getPlatform(); + + if (sourcePlatform === PLATFORM_FIREFOX) { + isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]; + } else { + isMetaMaskInternalProcess = + remotePort.sender.origin === + `chrome-extension://${extension.runtime.id}`; + } + if (isMetaMaskInternalProcess) { const portStream = new PortStream(remotePort); // communication with popup diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index c2f740e7e..c98c1683c 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -3,8 +3,8 @@ import { warn } from 'loglevel'; import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'; import { MINUTE } from '../../../shared/constants/time'; -import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util'; import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; // By default, poll every 3 minutes const DEFAULT_INTERVAL = MINUTE * 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/preferences.js b/app/scripts/controllers/preferences.js index 4a503e7e6..73e3b7963 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -68,6 +68,7 @@ export default class PreferencesController { ledgerTransportType: window.navigator.hid ? LEDGER_TRANSPORT_TYPES.WEBHID : LEDGER_TRANSPORT_TYPES.U2F, + theme: 'default', ...opts.initState, }; @@ -169,6 +170,15 @@ export default class PreferencesController { this.store.updateState({ eip1559V2Enabled: val }); } + /** + * Setter for the `theme` property + * + * @param {string} val - 'default' or 'dark' value based on the mode selected by user. + */ + setTheme(val) { + this.store.updateState({ theme: val }); + } + /** * Add new methodData to state, to avoid requesting this information again through Infura * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index d6c8c59bf..10e7ba215 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -351,4 +351,18 @@ describe('preferences controller', function () { ); }); }); + + describe('setTheme', function () { + it('should default to value "default"', function () { + const state = preferencesController.store.getState(); + assert.equal(state.theme, 'default'); + }); + + it('should set the setTheme property in state', function () { + const state = preferencesController.store.getState(); + assert.equal(state.theme, 'default'); + preferencesController.setTheme('dark'); + assert.equal(preferencesController.store.getState().theme, 'dark'); + }); + }); }); diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 8b16a86dc..6eb800793 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -28,7 +28,7 @@ import { } from '../../../ui/pages/swaps/swaps.util'; import fetchWithCache from '../../../ui/helpers/utils/fetch-with-cache'; import { MINUTE, SECOND } from '../../../shared/constants/time'; -import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import { NETWORK_EVENTS } from './network'; // The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 04eae26f8..92c308f29 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,13 +3,12 @@ import { ObservableStore } from '@metamask/obs-store'; import { bufferToHex, keccak, toBuffer, isHexString } from 'ethereumjs-util'; import EthQuery from 'ethjs-query'; import { ethErrors } from 'eth-rpc-errors'; -import abi from 'human-standard-token-abi'; import Common from '@ethereumjs/common'; import { TransactionFactory } from '@ethereumjs/tx'; -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, @@ -46,16 +45,15 @@ import { NETWORK_TYPE_RPC, CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, } from '../../../../shared/constants/network'; -import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; -import { readAddressAsContract } from '../../../../shared/modules/contract-utils'; -import { isEqualCaseInsensitive } from '../../../../ui/helpers/utils/util'; +import { + determineTransactionType, + isEIP1559Transaction, +} from '../../../../shared/modules/transaction.utils'; import TransactionStateManager from './tx-state-manager'; import TxGasUtil from './tx-gas-utils'; import PendingTransactionTracker from './pending-tx-tracker'; import * as txUtils from './lib/util'; -const hstInterface = new ethers.utils.Interface(abi); - const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory const SWAP_TRANSACTION_TYPES = [ @@ -130,6 +128,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 +347,260 @@ 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); + } + + /** + * updates the params that are editible in the send edit flow + * + * @param {string} txId - transaction id + * @param {object} editableParams - holds the editable parameters + * @param {object} editableParams.data + * @param {string} editableParams.from + * @param {string} editableParams.to + * @param {string} editableParams.value + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateEditableParams(txId, { data, from, to, value }) { + if (!this._checkIfTxStatusIsUnapproved(txId)) { + throw new Error( + 'Cannot call updateEditableParams on a transaction that is not in an unapproved state', + ); + } + + const editableParams = { + txParams: { + data, + from, + to, + value, + }, + }; + + // only update what is defined + editableParams.txParams = pickBy(editableParams.txParams); + const note = `Update Editable Params for ${txId}`; + this._updateTransaction(txId, editableParams, note); + return this._getTransaction(txId); + } + + /** + * 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 + * @param {string} txGasFees.gasLimit + * @param {string} txGasFees.gasPrice + * @param {string} txGasFees.maxPriorityFeePerGas + * @param {string} txGasFees.maxFeePerGas + * @param {string} txGasFees.estimateUsed + * @param {string} txGasFees.estimateSuggested + * @param {string} txGasFees.defaultGasEstimates + * @param {string} txGasFees.gas + * @param {string} txGasFees.originalGasEstimate + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateTransactionGasFees( + txId, + { + gas, + gasLimit, + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + estimateUsed, + estimateSuggested, + defaultGasEstimates, + originalGasEstimate, + }, + ) { + if (!this._checkIfTxStatusIsUnapproved(txId)) { + throw new Error( + 'Cannot call updateTransactionGasFees on a transaction that is not in an unapproved state', + ); + } + + 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); + return this._getTransaction(txId); + } + + /** + * 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 + * @param {string} txEstimateBaseFees.estimatedBaseFee + * @param {string} txEstimateBaseFees.decEstimatedBaseFee + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateTransactionEstimatedBaseFee( + txId, + { estimatedBaseFee, decEstimatedBaseFee }, + ) { + if (!this._checkIfTxStatusIsUnapproved(txId)) { + throw new Error( + 'Cannot call updateTransactionEstimatedBaseFee on a transaction that is not in an unapproved state', + ); + } + + 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); + return this._getTransaction(txId); + } + + /** + * 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 + * @param {string} swapApprovalTransaction.type + * @param {string} swapApprovalTransaction.sourceTokenSymbol + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateSwapApprovalTransaction(txId, { type, sourceTokenSymbol }) { + if (!this._checkIfTxStatusIsUnapproved(txId)) { + throw new Error( + 'Cannot call updateSwapApprovalTransaction on a transaction that is not in an unapproved state', + ); + } + + 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); + return this._getTransaction(txId); + } + + /** + * 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 + * @param {string} swapTransaction.sourceTokenSymbol + * @param {string} swapTransaction.destinationTokenSymbol + * @param {string} swapTransaction.type + * @param {string} swapTransaction.destinationTokenDecimals + * @param {string} swapTransaction.destinationTokenAddress + * @param {string} swapTransaction.swapMetaData + * @param {string} swapTransaction.swapTokenValue + * @param {string} swapTransaction.estimatedBaseFee + * @param {string} swapTransaction.approvalTxId + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateSwapTransaction( + txId, + { + sourceTokenSymbol, + destinationTokenSymbol, + type, + destinationTokenDecimals, + destinationTokenAddress, + swapMetaData, + swapTokenValue, + estimatedBaseFee, + approvalTxId, + }, + ) { + if (!this._checkIfTxStatusIsUnapproved(txId)) { + throw new Error( + 'Cannot call updateSwapTransaction on a transaction that is not in an unapproved state', + ); + } + 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); + return this._getTransaction(txId); + } + + /** + * updates a transaction's user settings only if the transaction state is unapproved + * + * @param {string} txId + * @param {object} userSettings - holds the metadata + * @param {string} userSettings.userEditedGasLimit + * @param {string} userSettings.userFeeLevel + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateTransactionUserSettings(txId, { userEditedGasLimit, userFeeLevel }) { + if (!this._checkIfTxStatusIsUnapproved(txId)) { + throw new Error( + 'Cannot call updateTransactionUserSettings on a transaction that is not in an unapproved state', + ); + } + + let userSettings = { userEditedGasLimit, userFeeLevel }; + // only update what is defined + userSettings = pickBy(userSettings); + + const note = `Update User Settings for ${txId}`; + this._updateTransaction(txId, userSettings, note); + return this._getTransaction(txId); + } + + // ==================================================================================================================================================== + /** * Validates and generates a txMeta with defaults and puts it in txStateManager * store. @@ -376,7 +630,7 @@ export default class TransactionController extends EventEmitter { * `generateTxMeta` adds the default txMeta properties to the passed object. * These include the tx's `id`. As we use the id for determining order of * txes in the tx-state-manager, it is necessary to call the asynchronous - * method `this._determineTransactionType` after `generateTxMeta`. + * method `determineTransactionType` after `generateTxMeta`. */ let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, @@ -404,8 +658,9 @@ export default class TransactionController extends EventEmitter { } } - const { type, getCodeResponse } = await this._determineTransactionType( + const { type, getCodeResponse } = await determineTransactionType( txParams, + this.query, ); txMeta.type = transactionType || type; @@ -1461,67 +1716,6 @@ export default class TransactionController extends EventEmitter { }); } - /** - * @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes - */ - - /** - * @typedef {Object} InferTransactionTypeResult - * @property {InferrableTransactionTypes} type - The type of transaction - * @property {string} getCodeResponse - The contract code, in hex format if - * it exists. '0x0' or '0x' are also indicators of non-existent contract - * code - */ - - /** - * Determines the type of the transaction by analyzing the txParams. - * This method will return one of the types defined in shared/constants/transactions - * It will never return TRANSACTION_TYPE_CANCEL or TRANSACTION_TYPE_RETRY as these - * represent specific events that we control from the extension and are added manually - * at transaction creation. - * - * @param {Object} txParams - Parameters for the transaction - * @returns {InferTransactionTypeResult} - */ - async _determineTransactionType(txParams) { - const { data, to } = txParams; - let name; - try { - name = data && hstInterface.parseTransaction({ data }).name; - } catch (error) { - log.debug('Failed to parse transaction data.', error, data); - } - - const tokenMethodName = [ - TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, - TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, - TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM, - ].find((methodName) => isEqualCaseInsensitive(methodName, name)); - - let result; - if (data && tokenMethodName) { - result = tokenMethodName; - } else if (data && !to) { - result = TRANSACTION_TYPES.DEPLOY_CONTRACT; - } - - let contractCode; - - if (!result) { - const { - contractCode: resultCode, - isContractAddress, - } = await readAddressAsContract(this.query, to); - - contractCode = resultCode; - result = isContractAddress - ? TRANSACTION_TYPES.CONTRACT_INTERACTION - : TRANSACTION_TYPES.SIMPLE_SEND; - } - - return { type: result, getCodeResponse: contractCode }; - } - /** * Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions * in the list have the same nonce @@ -1735,6 +1929,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..c0c189868 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 }); @@ -1303,156 +1305,6 @@ describe('Transaction Controller', function () { }); }); - describe('#_determineTransactionType', function () { - it('should return a simple send type when to is truthy but data is falsy', async function () { - const result = await txController._determineTransactionType({ - to: '0xabc', - data: '', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.SIMPLE_SEND, - getCodeResponse: null, - }); - }); - - it('should return a token transfer type when data is for the respective method call', async function () { - const result = await txController._determineTransactionType({ - to: '0xabc', - data: - '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, - getCodeResponse: undefined, - }); - }); - - it('should return a token approve type when data is for the respective method call', async function () { - const result = await txController._determineTransactionType({ - to: '0xabc', - data: - '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, - getCodeResponse: undefined, - }); - }); - - it('should return a contract deployment type when to is falsy and there is data', async function () { - const result = await txController._determineTransactionType({ - to: '', - data: '0xabd', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.DEPLOY_CONTRACT, - getCodeResponse: undefined, - }); - }); - - it('should return a simple send type with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () { - const result = await txController._determineTransactionType({ - to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', - data: '0xabd', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.SIMPLE_SEND, - getCodeResponse: '0x', - }); - }); - - it('should return a simple send type with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () { - const result = await txController._determineTransactionType({ - to: '0xabc', - data: '0xabd', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.SIMPLE_SEND, - getCodeResponse: null, - }); - }); - - it('should return a contract interaction type with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () { - const _providerResultStub = { - // 1 gwei - eth_gasPrice: '0x0de0b6b3a7640000', - // by default, all accounts are external accounts (not contracts) - eth_getCode: '0xa', - }; - const _provider = createTestProviderTools({ - scaffold: _providerResultStub, - }).provider; - const _fromAccount = getTestAccounts()[0]; - const _blockTrackerStub = new EventEmitter(); - _blockTrackerStub.getCurrentBlock = noop; - _blockTrackerStub.getLatestBlock = noop; - const _txController = new TransactionController({ - provider: _provider, - getGasPrice() { - return '0xee6b2800'; - }, - networkStore: new ObservableStore(currentNetworkId), - getCurrentChainId: () => currentChainId, - txHistoryLimit: 10, - blockTracker: _blockTrackerStub, - signTransaction: (ethTx) => - new Promise((resolve) => { - ethTx.sign(_fromAccount.key); - resolve(); - }), - getParticipateInMetrics: () => false, - }); - const result = await _txController._determineTransactionType({ - to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', - data: 'abd', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.CONTRACT_INTERACTION, - getCodeResponse: '0x0a', - }); - }); - - it('should return a contract interaction type with the correct getCodeResponse when to is a contract address and data is falsy', async function () { - const _providerResultStub = { - // 1 gwei - eth_gasPrice: '0x0de0b6b3a7640000', - // by default, all accounts are external accounts (not contracts) - eth_getCode: '0xa', - }; - const _provider = createTestProviderTools({ - scaffold: _providerResultStub, - }).provider; - const _fromAccount = getTestAccounts()[0]; - const _blockTrackerStub = new EventEmitter(); - _blockTrackerStub.getCurrentBlock = noop; - _blockTrackerStub.getLatestBlock = noop; - const _txController = new TransactionController({ - provider: _provider, - getGasPrice() { - return '0xee6b2800'; - }, - networkStore: new ObservableStore(currentNetworkId), - getCurrentChainId: () => currentChainId, - txHistoryLimit: 10, - blockTracker: _blockTrackerStub, - signTransaction: (ethTx) => - new Promise((resolve) => { - ethTx.sign(_fromAccount.key); - resolve(); - }), - getParticipateInMetrics: () => false, - }); - const result = await _txController._determineTransactionType({ - to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', - data: '', - }); - assert.deepEqual(result, { - type: TRANSACTION_TYPES.CONTRACT_INTERACTION, - getCodeResponse: '0x0a', - }); - }); - }); - describe('#getPendingTransactions', function () { it('should show only submitted and approved transactions as pending transaction', function () { txController.txStateManager._addTransactionsToState([ @@ -1616,6 +1468,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 +1545,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 +1632,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 +1711,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 +1790,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 +1851,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 +1922,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 +2025,202 @@ 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('throws error 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', + }); + + try { + txController.updateTransactionGasFees('4', { maxFeePerGas: '0x0088' }); + } catch (e) { + assert.equal( + e.message, + 'Cannot call updateTransactionGasFees on a transaction that is not in an unapproved state', + ); + } + }); + + 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'); + 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/lib/seed-phrase-verifier.js b/app/scripts/lib/seed-phrase-verifier.js index e1c18c1eb..225a57896 100644 --- a/app/scripts/lib/seed-phrase-verifier.js +++ b/app/scripts/lib/seed-phrase-verifier.js @@ -11,10 +11,10 @@ const seedPhraseVerifier = { * - The keyring always creates the accounts in the same sequence. * * @param {Array} createdAccounts - The accounts to restore - * @param {Buffer} seedPhrase - The seed words to verify, encoded as a Buffer - * @returns {Promise} + * @param {string} seedWords - The seed words to verify + * @returns {Promise} Promises undefined */ - async verifyAccounts(createdAccounts, seedPhrase) { + async verifyAccounts(createdAccounts, seedWords) { if (!createdAccounts || createdAccounts.length < 1) { throw new Error('No created accounts defined.'); } @@ -22,7 +22,7 @@ const seedPhraseVerifier = { const keyringController = new KeyringController({}); const Keyring = keyringController.getKeyringClassForType('HD Key Tree'); const opts = { - mnemonic: seedPhrase, + mnemonic: seedWords, numberOfAccounts: createdAccounts.length, }; diff --git a/app/scripts/lib/segment.js b/app/scripts/lib/segment.js index 3b7db70ce..bb52c42d8 100644 --- a/app/scripts/lib/segment.js +++ b/app/scripts/lib/segment.js @@ -1,8 +1,8 @@ import Analytics from 'analytics-node'; import { SECOND } from '../../../shared/constants/time'; -const isDevOrTestEnvironment = Boolean( - process.env.METAMASK_DEBUG || process.env.IN_TEST, +const isDevEnvironment = Boolean( + process.env.METAMASK_DEBUG && !process.env.IN_TEST, ); const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY ?? null; const SEGMENT_HOST = process.env.SEGMENT_HOST ?? null; @@ -82,7 +82,7 @@ export const createSegmentMock = (flushAt = SEGMENT_FLUSH_AT) => { }; export const segment = - !SEGMENT_WRITE_KEY || (isDevOrTestEnvironment && !SEGMENT_HOST) + !SEGMENT_WRITE_KEY || (isDevEnvironment && !SEGMENT_HOST) ? createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL) : new Analytics(SEGMENT_WRITE_KEY, { host: SEGMENT_HOST, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ccfee5ae5..8bb569897 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -9,7 +9,11 @@ import createFilterMiddleware from 'eth-json-rpc-filters'; import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager'; import { providerAsMiddleware } from 'eth-json-rpc-middleware'; import KeyringController from 'eth-keyring-controller'; -import { errorCodes as rpcErrorCodes, ethErrors } from 'eth-rpc-errors'; +import { + errorCodes as rpcErrorCodes, + EthereumRpcError, + ethErrors, +} from 'eth-rpc-errors'; import { Mutex } from 'await-semaphore'; import { stripHexPrefix } from 'ethereumjs-util'; import log from 'loglevel'; @@ -79,7 +83,7 @@ import { import { hexToDecimal } from '../../ui/helpers/utils/conversions.util'; import { getTokenValueParam } from '../../ui/helpers/utils/token-util'; import { getTransactionData } from '../../ui/helpers/utils/transactions.util'; -import { isEqualCaseInsensitive } from '../../ui/helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; @@ -711,6 +715,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()); @@ -1280,7 +1286,6 @@ export default class MetamaskController extends EventEmitter { appStateController, collectiblesController, collectibleDetectionController, - assetsContractController, currencyRateController, detectTokensController, ensController, @@ -1433,11 +1438,10 @@ export default class MetamaskController extends EventEmitter { setEIP1559V2Enabled: preferencesController.setEIP1559V2Enabled.bind( preferencesController, ), + setTheme: preferencesController.setTheme.bind(preferencesController), // AssetsContractController - getTokenStandardAndDetails: assetsContractController.getTokenStandardAndDetails.bind( - assetsContractController, - ), + getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), // CollectiblesController addCollectible: collectiblesController.addCollectible.bind( @@ -1534,6 +1538,20 @@ export default class MetamaskController extends EventEmitter { ), getTransactions: txController.getTransactions.bind(txController), + updateEditableParams: txController.updateEditableParams.bind( + txController, + ), + updateTransactionGasFees: txController.updateTransactionGasFees.bind( + txController, + ), + + updateSwapApprovalTransaction: txController.updateSwapApprovalTransaction.bind( + txController, + ), + updateSwapTransaction: txController.updateSwapTransaction.bind( + txController, + ), + // messageManager signMessage: this.signMessage.bind(this), cancelMessage: this.cancelMessage.bind(this), @@ -1718,7 +1736,12 @@ export default class MetamaskController extends EventEmitter { resolvePendingApproval: approvalController.accept.bind( approvalController, ), - rejectPendingApproval: approvalController.reject.bind(approvalController), + rejectPendingApproval: async (id, error) => { + approvalController.reject( + id, + new EthereumRpcError(error.code, error.message, error.data), + ); + }, // Notifications updateViewedNotifications: notificationController.updateViewed.bind( @@ -1760,6 +1783,19 @@ export default class MetamaskController extends EventEmitter { }; } + async getTokenStandardAndDetails(address, userAddress, tokenId) { + const details = await this.assetsContractController.getTokenStandardAndDetails( + address, + userAddress, + tokenId, + ); + return { + ...details, + decimals: details?.decimals?.toString(10), + balance: details?.balance?.toString(10), + }; + } + //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= @@ -1803,16 +1839,13 @@ export default class MetamaskController extends EventEmitter { * Create a new Vault and restore an existent keyring. * * @param {string} password - * @param {number[]} encodedSeedPhrase - The seed phrase, encoded as an array - * of UTF-8 bytes. + * @param {string} seed */ - async createNewVaultAndRestore(password, encodedSeedPhrase) { + async createNewVaultAndRestore(password, seed) { const releaseLock = await this.createVaultMutex.acquire(); try { let accounts, lastBalance; - const seedPhraseAsBuffer = Buffer.from(encodedSeedPhrase); - const { keyringController } = this; // clear known identities @@ -1833,7 +1866,7 @@ export default class MetamaskController extends EventEmitter { // create new vault const vault = await keyringController.createNewVaultAndRestore( password, - seedPhraseAsBuffer, + seed, ); const ethQuery = new EthQuery(this.provider); @@ -2200,6 +2233,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 * @@ -2293,8 +2374,7 @@ export default class MetamaskController extends EventEmitter { * * Called when the first account is created and on unlocking the vault. * - * @returns {Promise} The seed phrase to be confirmed by the user, - * encoded as an array of UTF-8 bytes. + * @returns {Promise} Seed phrase to be confirmed by the user. */ async verifySeedPhrase() { const primaryKeyring = this.keyringController.getKeyringsByType( @@ -2305,7 +2385,7 @@ export default class MetamaskController extends EventEmitter { } const serialized = await primaryKeyring.serialize(); - const seedPhraseAsBuffer = Buffer.from(serialized.mnemonic); + const seedWords = serialized.mnemonic; const accounts = await primaryKeyring.getAccounts(); if (accounts.length < 1) { @@ -2313,8 +2393,8 @@ export default class MetamaskController extends EventEmitter { } try { - await seedPhraseVerifier.verifyAccounts(accounts, seedPhraseAsBuffer); - return Array.from(seedPhraseAsBuffer.values()); + await seedPhraseVerifier.verifyAccounts(accounts, seedWords); + return seedWords; } catch (err) { log.error(err.message); throw err; diff --git a/development/build/scripts.js b/development/build/scripts.js index 035c38969..063fc5a7e 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -34,6 +34,7 @@ const metamaskrc = require('rc')('metamask', { INFURA_PROD_PROJECT_ID: process.env.INFURA_PROD_PROJECT_ID, ONBOARDING_V2: process.env.ONBOARDING_V2, COLLECTIBLES_V1: process.env.COLLECTIBLES_V1, + DARK_MODE_V1: process.env.DARK_MODE_V1, SEGMENT_HOST: process.env.SEGMENT_HOST, SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY, SEGMENT_BETA_WRITE_KEY: process.env.SEGMENT_BETA_WRITE_KEY, @@ -810,6 +811,7 @@ function getEnvironmentVariables({ buildType, devMode, testing, version }) { SWAPS_USE_DEV_APIS: process.env.SWAPS_USE_DEV_APIS === '1', ONBOARDING_V2: metamaskrc.ONBOARDING_V2 === '1', COLLECTIBLES_V1: metamaskrc.COLLECTIBLES_V1 === '1', + DARK_MODE_V1: metamaskrc.DARK_MODE_V1 === '1', }; } 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 deleted file mode 100644 index 1e466d36e..000000000 --- a/development/mock-e2e.js +++ /dev/null @@ -1,32 +0,0 @@ -function setupMocking(server) { - server.forAnyRequest().thenPassThrough(); - - server - .forOptions('https://gas-api.metaswap.codefi.network/networks/1/gasPrices') - .thenCallback(() => { - return { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - 'Access-Control-Allow-Headers': 'content-type,x-client-id', - }, - statusCode: 200, - }; - }); - - server - .forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices') - .thenCallback(() => { - return { - headers: { 'Access-Control-Allow-Origin': '*' }, - statusCode: 200, - json: { - SafeGasPrice: '1', - ProposeGasPrice: '2', - FastGasPrice: '3', - }, - }; - }); -} - -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/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ba9056f3e..2b616f2e8 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1178,9 +1178,6 @@ } }, "bip39": { - "globals": { - "console.log": true - }, "packages": { "buffer": true, "create-hash": true, @@ -1895,7 +1892,6 @@ "eth-hd-keyring": { "packages": { "bip39": true, - "buffer": true, "eth-sig-util": true, "eth-simple-keyring": true, "ethereumjs-wallet": true @@ -1954,7 +1950,6 @@ "packages": { "bip39": true, "browser-passworder": true, - "buffer": true, "eth-hd-keyring": true, "eth-sig-util": true, "eth-simple-keyring": true, @@ -1970,17 +1965,19 @@ "browser": true, "clearInterval": true, "open": true, + "rlp.encode": true, "setInterval": true }, "packages": { "@ethereumjs/common": true, "@ethereumjs/tx": true, - "bignumber.js": true, + "bn.js": true, "buffer": true, "crypto-browserify": true, "ethereumjs-util": true, "events": true, - "gridplus-sdk": true + "gridplus-sdk": true, + "secp256k1": true } }, "eth-method-registry": { @@ -2349,6 +2346,8 @@ "setTimeout": true }, "packages": { + "@ethereumjs/common": true, + "@ethereumjs/tx": true, "aes-js": true, "bech32": true, "bignumber.js": true, @@ -2361,6 +2360,7 @@ "eth-eip712-util-browser": true, "hash.js": true, "js-sha3": true, + "rlp": true, "rlp-browser": true, "secp256k1": true, "superagent": true @@ -4628,6 +4628,9 @@ } }, "rlp": { + "globals": { + "TextEncoder": true + }, "packages": { "bn.js": true, "buffer": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 3757925a1..92099f02c 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1196,9 +1196,6 @@ } }, "bip39": { - "globals": { - "console.log": true - }, "packages": { "buffer": true, "create-hash": true, @@ -1913,7 +1910,6 @@ "eth-hd-keyring": { "packages": { "bip39": true, - "buffer": true, "eth-sig-util": true, "eth-simple-keyring": true, "ethereumjs-wallet": true @@ -1972,7 +1968,6 @@ "packages": { "bip39": true, "browser-passworder": true, - "buffer": true, "eth-hd-keyring": true, "eth-sig-util": true, "eth-simple-keyring": true, @@ -1988,17 +1983,19 @@ "browser": true, "clearInterval": true, "open": true, + "rlp.encode": true, "setInterval": true }, "packages": { "@ethereumjs/common": true, "@ethereumjs/tx": true, - "bignumber.js": true, + "bn.js": true, "buffer": true, "crypto-browserify": true, "ethereumjs-util": true, "events": true, - "gridplus-sdk": true + "gridplus-sdk": true, + "secp256k1": true } }, "eth-method-registry": { @@ -2367,6 +2364,8 @@ "setTimeout": true }, "packages": { + "@ethereumjs/common": true, + "@ethereumjs/tx": true, "aes-js": true, "bech32": true, "bignumber.js": true, @@ -2379,6 +2378,7 @@ "eth-eip712-util-browser": true, "hash.js": true, "js-sha3": true, + "rlp": true, "rlp-browser": true, "secp256k1": true, "superagent": true @@ -4646,6 +4646,9 @@ } }, "rlp": { + "globals": { + "TextEncoder": true + }, "packages": { "bn.js": true, "buffer": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ba9056f3e..2b616f2e8 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1178,9 +1178,6 @@ } }, "bip39": { - "globals": { - "console.log": true - }, "packages": { "buffer": true, "create-hash": true, @@ -1895,7 +1892,6 @@ "eth-hd-keyring": { "packages": { "bip39": true, - "buffer": true, "eth-sig-util": true, "eth-simple-keyring": true, "ethereumjs-wallet": true @@ -1954,7 +1950,6 @@ "packages": { "bip39": true, "browser-passworder": true, - "buffer": true, "eth-hd-keyring": true, "eth-sig-util": true, "eth-simple-keyring": true, @@ -1970,17 +1965,19 @@ "browser": true, "clearInterval": true, "open": true, + "rlp.encode": true, "setInterval": true }, "packages": { "@ethereumjs/common": true, "@ethereumjs/tx": true, - "bignumber.js": true, + "bn.js": true, "buffer": true, "crypto-browserify": true, "ethereumjs-util": true, "events": true, - "gridplus-sdk": true + "gridplus-sdk": true, + "secp256k1": true } }, "eth-method-registry": { @@ -2349,6 +2346,8 @@ "setTimeout": true }, "packages": { + "@ethereumjs/common": true, + "@ethereumjs/tx": true, "aes-js": true, "bech32": true, "bignumber.js": true, @@ -2361,6 +2360,7 @@ "eth-eip712-util-browser": true, "hash.js": true, "js-sha3": true, + "rlp": true, "rlp-browser": true, "secp256k1": true, "superagent": true @@ -4628,6 +4628,9 @@ } }, "rlp": { + "globals": { + "TextEncoder": true + }, "packages": { "bn.js": true, "buffer": true 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 e91927dcf..a10948d82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "10.11.4", + "version": "10.12.0", "private": true, "repository": { "type": "git", @@ -14,12 +14,11 @@ "dist": "yarn build prod", "build": "yarn lavamoat:build", "build:dev": "node development/build/index.js", - "start:test": "yarn build testDev", + "start:test": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' yarn build testDev", "benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js", "benchmark:firefox": "SELENIUM_BROWSER=firefox node test/e2e/benchmark.js", - "build:test": "yarn build test", + "build:test": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' yarn build test", "build:test:flask": "yarn build test --build-type flask", - "build:test:metrics": "SEGMENT_HOST='http://localhost:9090' SEGMENT_WRITE_KEY='FAKE' yarn build test", "test": "yarn lint && yarn test:unit && yarn test:unit:jest", "dapp": "node development/static-server.js node_modules/@metamask/test-dapp/dist --port 8080", "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'", @@ -112,6 +111,7 @@ "@keystonehq/metamask-airgapped-keyring": "0.2.1", "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.31.0", + "@metamask/design-tokens": "^1.3.0", "@metamask/controllers": "^27.0.0", "@metamask/eth-ledger-bridge-keyring": "^0.10.0", "@metamask/eth-token-tracker": "^4.0.0", @@ -153,7 +153,7 @@ "eth-json-rpc-infura": "^5.1.0", "eth-json-rpc-middleware": "^8.0.0", "eth-keyring-controller": "^6.2.0", - "eth-lattice-keyring": "^0.5.0", + "eth-lattice-keyring": "^0.6.1", "eth-method-registry": "^2.0.0", "eth-query": "^2.1.2", "eth-rpc-errors": "^4.0.2", @@ -223,7 +223,8 @@ "uuid": "^8.3.2", "valid-url": "^1.0.9", "web3": "^0.20.7", - "web3-stream-provider": "^4.0.0" + "web3-stream-provider": "^4.0.0", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@babel/code-frame": "^7.12.13", @@ -351,6 +352,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/patches/bip39+2.5.0.patch b/patches/bip39+2.5.0.patch deleted file mode 100644 index 2976f3bb2..000000000 --- a/patches/bip39+2.5.0.patch +++ /dev/null @@ -1,99 +0,0 @@ -diff --git a/node_modules/bip39/index.js b/node_modules/bip39/index.js -index aa0f29f..bee8008 100644 ---- a/node_modules/bip39/index.js -+++ b/node_modules/bip39/index.js -@@ -48,7 +48,9 @@ function salt (password) { - } - - function mnemonicToSeed (mnemonic, password) { -- var mnemonicBuffer = Buffer.from(unorm.nfkd(mnemonic), 'utf8') -+ var mnemonicBuffer = typeof mnemonic === 'string' -+ ? Buffer.from(unorm.nfkd(mnemonic), 'utf8') -+ : mnemonic - var saltBuffer = Buffer.from(salt(unorm.nfkd(password)), 'utf8') - - return pbkdf2(mnemonicBuffer, saltBuffer, 2048, 64, 'sha512') -@@ -61,12 +63,28 @@ function mnemonicToSeedHex (mnemonic, password) { - function mnemonicToEntropy (mnemonic, wordlist) { - wordlist = wordlist || DEFAULT_WORDLIST - -- var words = unorm.nfkd(mnemonic).split(' ') -+ var mnemonicAsBuffer = typeof mnemonic === 'string' -+ ? Buffer.from(unorm.nfkd(mnemonic), 'utf8') -+ : mnemonic -+ -+ var words = []; -+ var currentWord = []; -+ for (const byte of mnemonicAsBuffer.values()) { -+ // split at space or \u3000 (ideographic space, for Japanese wordlists) -+ if (byte === 0x20 || byte === 0x3000) { -+ words.push(Buffer.from(currentWord)); -+ currentWord = []; -+ } else { -+ currentWord.push(byte); -+ } -+ } -+ words.push(Buffer.from(currentWord)); -+ - if (words.length % 3 !== 0) throw new Error(INVALID_MNEMONIC) - - // convert word indices to 11 bit binary strings - var bits = words.map(function (word) { -- var index = wordlist.indexOf(word) -+ var index = wordlist.indexOf(word.toString('utf8')) - if (index === -1) throw new Error(INVALID_MNEMONIC) - - return lpad(index.toString(2), '0', 11) -@@ -104,12 +122,41 @@ function entropyToMnemonic (entropy, wordlist) { - - var bits = entropyBits + checksumBits - var chunks = bits.match(/(.{1,11})/g) -- var words = chunks.map(function (binary) { -+ var wordsAsBuffers = chunks.map(function (binary) { - var index = binaryToByte(binary) -- return wordlist[index] -+ return Buffer.from(wordlist[index], 'utf8') - }) - -- return wordlist === JAPANESE_WORDLIST ? words.join('\u3000') : words.join(' ') -+ var bufferSize = wordsAsBuffers.reduce(function (bufferSize, wordAsBuffer, i) { -+ var shouldAddSeparator = i < wordsAsBuffers.length - 1 -+ return ( -+ bufferSize + -+ wordAsBuffer.length + -+ (shouldAddSeparator ? 1 : 0) -+ ) -+ }, 0) -+ var separator = wordlist === JAPANESE_WORDLIST ? '\u3000' : ' ' -+ var result = wordsAsBuffers.reduce(function (result, wordAsBuffer, i) { -+ var shouldAddSeparator = i < wordsAsBuffers.length - 1 -+ result.workingBuffer.set(wordAsBuffer, result.offset) -+ if (shouldAddSeparator) { -+ result.workingBuffer.write( -+ separator, -+ result.offset + wordAsBuffer.length, -+ separator.length, -+ 'utf8' -+ ) -+ } -+ return { -+ workingBuffer: result.workingBuffer, -+ offset: ( -+ result.offset + -+ wordAsBuffer.length + -+ (shouldAddSeparator ? 1 : 0) -+ ) -+ } -+ }, { workingBuffer: Buffer.alloc(bufferSize), offset: 0 }) -+ return result.workingBuffer; - } - - function generateMnemonic (strength, rng, wordlist) { -@@ -124,6 +171,7 @@ function validateMnemonic (mnemonic, wordlist) { - try { - mnemonicToEntropy(mnemonic, wordlist) - } catch (e) { -+ console.log('could not validate mnemonic', e) - return false - } - diff --git a/patches/eth-hd-keyring+3.6.0.patch b/patches/eth-hd-keyring+3.6.0.patch deleted file mode 100644 index 211cb89dd..000000000 --- a/patches/eth-hd-keyring+3.6.0.patch +++ /dev/null @@ -1,43 +0,0 @@ -diff --git a/node_modules/eth-hd-keyring/index.js b/node_modules/eth-hd-keyring/index.js -index 19d1d7f..350d6b8 100644 ---- a/node_modules/eth-hd-keyring/index.js -+++ b/node_modules/eth-hd-keyring/index.js -@@ -17,8 +17,11 @@ class HdKeyring extends SimpleKeyring { - } - - serialize () { -+ const mnemonicAsBuffer = typeof this.mnemonic === 'string' -+ ? Buffer.from(this.mnemonic, 'utf8') -+ : this.mnemonic - return Promise.resolve({ -- mnemonic: this.mnemonic, -+ mnemonic: Array.from(mnemonicAsBuffer.values()), - numberOfAccounts: this.wallets.length, - hdPath: this.hdPath, - }) -@@ -69,9 +72,22 @@ class HdKeyring extends SimpleKeyring { - - /* PRIVATE METHODS */ - -- _initFromMnemonic (mnemonic) { -- this.mnemonic = mnemonic -- const seed = bip39.mnemonicToSeed(mnemonic) -+ /** -+ * Sets appropriate properties for the keyring based on the given -+ * BIP39-compliant mnemonic. -+ * -+ * @param {string|Array|Buffer} mnemonic - A seed phrase represented -+ * as a string, an array of UTF-8 bytes, or a Buffer. -+ */ -+ _initFromMnemonic(mnemonic) { -+ if (typeof mnemonic === 'string') { -+ this.mnemonic = Buffer.from(mnemonic, 'utf8') -+ } else if (Array.isArray(mnemonic)) { -+ this.mnemonic = Buffer.from(mnemonic) -+ } else { -+ this.mnemonic = mnemonic -+ } -+ const seed = bip39.mnemonicToSeed(this.mnemonic) - this.hdWallet = hdkey.fromMasterSeed(seed) - this.root = this.hdWallet.derivePath(this.hdPath) - } diff --git a/patches/eth-keyring-controller+6.2.1.patch b/patches/eth-keyring-controller+6.2.1.patch deleted file mode 100644 index aec0c7168..000000000 --- a/patches/eth-keyring-controller+6.2.1.patch +++ /dev/null @@ -1,37 +0,0 @@ -diff --git a/node_modules/eth-keyring-controller/index.js b/node_modules/eth-keyring-controller/index.js -index 250ab98..38615aa 100644 ---- a/node_modules/eth-keyring-controller/index.js -+++ b/node_modules/eth-keyring-controller/index.js -@@ -84,15 +84,20 @@ class KeyringController extends EventEmitter { - * - * @emits KeyringController#unlock - * @param {string} password - The password to encrypt the vault with -- * @param {string} seed - The BIP44-compliant seed phrase. -+ * @param {string|Array} seedPhrase - The BIP39-compliant seed phrase, -+ * either as a string or an array of UTF-8 bytes that represent the string. - * @returns {Promise} A Promise that resolves to the state. - */ -- createNewVaultAndRestore (password, seed) { -+ createNewVaultAndRestore(password, seedPhrase) { -+ const seedPhraseAsBuffer = typeof seedPhrase === 'string' -+ ? Buffer.from(seedPhrase, 'utf8') -+ : Buffer.from(seedPhrase) -+ - if (typeof password !== 'string') { - return Promise.reject(new Error('Password must be text.')) - } - -- if (!bip39.validateMnemonic(seed)) { -+ if (!bip39.validateMnemonic(seedPhraseAsBuffer)) { - return Promise.reject(new Error('Seed phrase is invalid.')) - } - -@@ -101,7 +106,7 @@ class KeyringController extends EventEmitter { - return this.persistAllKeyrings(password) - .then(() => { - return this.addNewKeyring('HD Key Tree', { -- mnemonic: seed, -+ mnemonic: seedPhraseAsBuffer, - numberOfAccounts: 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/shared/modules/string-utils.js b/shared/modules/string-utils.js new file mode 100644 index 000000000..fc496227f --- /dev/null +++ b/shared/modules/string-utils.js @@ -0,0 +1,6 @@ +export function isEqualCaseInsensitive(value1, value2) { + if (typeof value1 !== 'string' || typeof value2 !== 'string') { + return false; + } + return value1.toLowerCase() === value2.toLowerCase(); +} diff --git a/shared/modules/transaction.utils.js b/shared/modules/transaction.utils.js index bd0b6fdf4..8995b6c02 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -1,4 +1,24 @@ import { isHexString } from 'ethereumjs-util'; +import { ethers } from 'ethers'; +import abi from 'human-standard-token-abi'; +import log from 'loglevel'; +import { TRANSACTION_TYPES } from '../constants/transaction'; +import { readAddressAsContract } from './contract-utils'; +import { isEqualCaseInsensitive } from './string-utils'; + +/** + * @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes + */ + +/** + * @typedef {Object} InferTransactionTypeResult + * @property {InferrableTransactionTypes} type - The type of transaction + * @property {string} getCodeResponse - The contract code, in hex format if + * it exists. '0x0' or '0x' are also indicators of non-existent contract + * code + */ + +const hstInterface = new ethers.utils.Interface(abi); export function transactionMatchesNetwork(transaction, chainId, networkId) { if (typeof transaction.chainId !== 'undefined') { @@ -61,3 +81,53 @@ export function txParamsAreDappSuggested(transaction) { transaction?.dappSuggestedGasFees?.maxFeePerGas === maxFeePerGas) ); } + +/** + * Determines the type of the transaction by analyzing the txParams. + * This method will return one of the types defined in shared/constants/transactions + * It will never return TRANSACTION_TYPE_CANCEL or TRANSACTION_TYPE_RETRY as these + * represent specific events that we control from the extension and are added manually + * at transaction creation. + * + * @param {Object} txParams - Parameters for the transaction + * @param {EthQuery} query - EthQuery instance + * @returns {InferTransactionTypeResult} + */ +export async function determineTransactionType(txParams, query) { + const { data, to } = txParams; + let name; + try { + name = data && hstInterface.parseTransaction({ data }).name; + } catch (error) { + log.debug('Failed to parse transaction data.', error, data); + } + + const tokenMethodName = [ + TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, + TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, + TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM, + ].find((methodName) => isEqualCaseInsensitive(methodName, name)); + + let result; + if (data && tokenMethodName) { + result = tokenMethodName; + } else if (data && !to) { + result = TRANSACTION_TYPES.DEPLOY_CONTRACT; + } + + let contractCode; + + if (!result) { + const { + contractCode: resultCode, + isContractAddress, + } = await readAddressAsContract(query, to); + + contractCode = resultCode; + result = isContractAddress + ? TRANSACTION_TYPES.CONTRACT_INTERACTION + : TRANSACTION_TYPES.SIMPLE_SEND; + } + + return { type: result, getCodeResponse: contractCode }; +} diff --git a/shared/modules/transaction.utils.test.js b/shared/modules/transaction.utils.test.js index 3a333caa7..f6d816160 100644 --- a/shared/modules/transaction.utils.test.js +++ b/shared/modules/transaction.utils.test.js @@ -1,4 +1,11 @@ -import { isEIP1559Transaction, isLegacyTransaction } from './transaction.utils'; +import EthQuery from 'ethjs-query'; +import { createTestProviderTools } from '../../test/stub/provider'; +import { TRANSACTION_TYPES } from '../constants/transaction'; +import { + determineTransactionType, + isEIP1559Transaction, + isLegacyTransaction, +} from './transaction.utils'; describe('Transaction.utils', function () { describe('isEIP1559Transaction', function () { @@ -80,4 +87,143 @@ describe('Transaction.utils', function () { ).toBe(false); }); }); + + describe('determineTransactionType', function () { + const genericProvider = createTestProviderTools().provider; + const query = new EthQuery(genericProvider); + + it('should return a simple send type when to is truthy but data is falsy', async function () { + const result = await determineTransactionType( + { + to: '0xabc', + data: '', + }, + query, + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.SIMPLE_SEND, + getCodeResponse: null, + }); + }); + + it('should return a token transfer type when data is for the respective method call', async function () { + const result = await determineTransactionType( + { + to: '0xabc', + data: + '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', + }, + query, + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, + getCodeResponse: undefined, + }); + }); + + it('should return a token approve type when data is for the respective method call', async function () { + const result = await determineTransactionType( + { + to: '0xabc', + data: + '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', + }, + query, + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, + getCodeResponse: undefined, + }); + }); + + it('should return a contract deployment type when to is falsy and there is data', async function () { + const result = await determineTransactionType( + { + to: '', + data: '0xabd', + }, + query, + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.DEPLOY_CONTRACT, + getCodeResponse: undefined, + }); + }); + + it('should return a simple send type with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () { + const result = await determineTransactionType( + { + to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', + data: '0xabd', + }, + query, + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.SIMPLE_SEND, + getCodeResponse: '0x', + }); + }); + + it('should return a simple send type with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () { + const result = await determineTransactionType( + { + to: '0xabc', + data: '0xabd', + }, + query, + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.SIMPLE_SEND, + getCodeResponse: null, + }); + }); + + it('should return a contract interaction type with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () { + const _providerResultStub = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0xa', + }; + const _provider = createTestProviderTools({ + scaffold: _providerResultStub, + }).provider; + + const result = await determineTransactionType( + { + to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', + data: 'abd', + }, + new EthQuery(_provider), + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.CONTRACT_INTERACTION, + getCodeResponse: '0x0a', + }); + }); + + it('should return a contract interaction type with the correct getCodeResponse when to is a contract address and data is falsy', async function () { + const _providerResultStub = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0xa', + }; + const _provider = createTestProviderTools({ + scaffold: _providerResultStub, + }).provider; + + const result = await determineTransactionType( + { + to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', + data: '', + }, + new EthQuery(_provider), + ); + expect(result).toMatchObject({ + type: TRANSACTION_TYPES.CONTRACT_INTERACTION, + getCodeResponse: '0x0a', + }); + }); + }); }); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 148964b03..523cafdb0 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -1,12 +1,9 @@ const path = require('path'); -const sinon = require('sinon'); const BigNumber = require('bignumber.js'); const mockttp = require('mockttp'); const createStaticServer = require('../../development/create-static-server'); -const { - createSegmentServer, -} = require('../../development/lib/create-segment-server'); -const { setupMocking } = require('../../development/mock-e2e'); +const enLocaleMessages = require('../../app/_locales/en/messages.json'); +const { setupMocking } = require('./mock-e2e'); const Ganache = require('./ganache'); const FixtureServer = require('./fixture-server'); const { buildWebDriver } = require('./webdriver'); @@ -15,6 +12,7 @@ const { ensureXServerIsRunning } = require('./x-server'); const tinyDelayMs = 200; const regularDelayMs = tinyDelayMs * 2; const largeDelayMs = regularDelayMs * 2; +const veryLargeDelayMs = largeDelayMs * 2; const dappPort = 8080; const convertToHexValue = (val) => `0x${new BigNumber(val, 10).toString(16)}`; @@ -25,18 +23,19 @@ async function withFixtures(options, testSuite) { fixtures, ganacheOptions, driverOptions, - mockSegment, title, failOnConsoleError = true, dappPath = undefined, + testSpecificMock = function () { + // do nothing. + }, } = options; const fixtureServer = new FixtureServer(); const ganacheServer = new Ganache(); + const https = await mockttp.generateCACertificate(); + const mockServer = mockttp.getLocal({ https, cors: true }); let secondaryGanacheServer; let dappServer; - let segmentServer; - let segmentStub; - let mockServer; let webDriver; let failed = false; @@ -76,21 +75,8 @@ async function withFixtures(options, testSuite) { dappServer.on('error', reject); }); } - if (mockSegment) { - segmentStub = sinon.stub(); - segmentServer = createSegmentServer((_request, response, events) => { - for (const event of events) { - segmentStub(event); - } - response.statusCode = 200; - response.end(); - }); - await segmentServer.start(9090); - } - const https = await mockttp.generateCACertificate(); - mockServer = mockttp.getLocal({ https }); + await setupMocking(mockServer, testSpecificMock); await mockServer.start(8000); - setupMocking(mockServer); if ( process.env.SELENIUM_BROWSER === 'chrome' && process.env.CI === 'true' @@ -102,7 +88,6 @@ async function withFixtures(options, testSuite) { await testSuite({ driver, - segmentStub, mockServer, }); @@ -150,12 +135,7 @@ async function withFixtures(options, testSuite) { }); }); } - if (segmentServer) { - await segmentServer.stop(); - } - if (mockServer) { - await mockServer.stop(); - } + await mockServer.stop(); } } } @@ -203,12 +183,80 @@ 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.pasteIntoField( + '[data-testid="import-srp__srp-word-0"]', + 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, tinyDelayMs, regularDelayMs, largeDelayMs, + veryLargeDelayMs, withFixtures, connectDappWithExtensionPopup, + completeImportSRPOnboardingFlow, }; diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 26db7f7d8..f80d737f8 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -3,7 +3,12 @@ const path = require('path'); const enLocaleMessages = require('../../app/_locales/en/messages.json'); const createStaticServer = require('../../development/create-static-server'); -const { tinyDelayMs, regularDelayMs, largeDelayMs } = require('./helpers'); +const { + tinyDelayMs, + regularDelayMs, + largeDelayMs, + veryLargeDelayMs, +} = require('./helpers'); const { buildWebDriver } = require('./webdriver'); const Ganache = require('./ganache'); const { ensureXServerIsRunning } = require('./x-server'); @@ -191,12 +196,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); @@ -274,7 +276,7 @@ describe('MetaMask', function () { const gasPriceInput = inputs[1]; await gasLimitInput.fill('4700000'); await gasPriceInput.fill('20'); - await driver.delay(1000); + await driver.delay(veryLargeDelayMs); await driver.clickElement({ text: 'Save', tag: 'button' }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); @@ -345,10 +347,11 @@ describe('MetaMask', function () { // Continue to next screen await driver.delay(largeDelayMs); await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.delay(largeDelayMs); }); it('displays the token transfer data', async function () { + await driver.delay(largeDelayMs); await driver.clickElement({ text: 'Hex', tag: 'button' }); await driver.delay(regularDelayMs); @@ -388,7 +391,7 @@ describe('MetaMask', function () { const gasPriceInput = inputs[1]; await gasLimitInput.fill('100000'); await gasPriceInput.fill('100'); - await driver.delay(1000); + await driver.delay(veryLargeDelayMs); await driver.clickElement({ text: 'Save', tag: 'button' }); }); @@ -451,19 +454,20 @@ describe('MetaMask', function () { }); it('customizes gas', async function () { + await driver.delay(veryLargeDelayMs); await driver.clickElement({ text: 'Edit', tag: 'button' }); - await driver.delay(largeDelayMs); + await driver.delay(veryLargeDelayMs); await driver.clickElement( { text: 'Edit suggested gas fee', tag: 'button' }, 10000, ); - await driver.delay(1000); + await driver.delay(veryLargeDelayMs); const inputs = await driver.findElements('input[type="number"]'); const gasLimitInput = inputs[0]; const gasPriceInput = inputs[1]; await gasLimitInput.fill('60000'); await gasPriceInput.fill('10'); - await driver.delay(1000); + await driver.delay(veryLargeDelayMs); await driver.clickElement({ text: 'Save', tag: 'button' }); await driver.findElement({ tag: 'span', text: '0.0006' }); }); @@ -588,7 +592,7 @@ describe('MetaMask', function () { await gasLimitInput.fill('60001'); - await driver.delay(1000); + await driver.delay(veryLargeDelayMs); await driver.clickElement({ text: 'Save', tag: 'button' }); @@ -754,7 +758,7 @@ describe('MetaMask', function () { }); it('submits the transaction', async function () { - await driver.delay(1000); + await driver.delay(veryLargeDelayMs); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); }); diff --git a/test/e2e/metrics.spec.js b/test/e2e/metrics.spec.js deleted file mode 100644 index 141b56192..000000000 --- a/test/e2e/metrics.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -const { strict: assert } = require('assert'); -const waitUntilCalled = require('../lib/wait-until-called'); -const { convertToHexValue, withFixtures, tinyDelayMs } = require('./helpers'); - -/** - * WARNING: These tests must be run using a build created with `yarn build:test:metrics`, so that it has - * the correct Segment host and write keys set. Otherwise this test will fail. - */ -describe('Segment metrics', function () { - this.timeout(0); - - it('should send first three Page metric events upon fullscreen page load', async function () { - const ganacheOptions = { - accounts: [ - { - secretKey: - '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', - balance: convertToHexValue(25000000000000000000), - }, - ], - }; - await withFixtures( - { - fixtures: 'metrics-enabled', - ganacheOptions, - title: this.test.title, - mockSegment: true, - }, - async ({ driver, segmentStub }) => { - const threeSegmentEventsReceived = waitUntilCalled(segmentStub, null, { - callCount: 3, - }); - await driver.delay(tinyDelayMs); - await driver.navigate(); - - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); - - await threeSegmentEventsReceived(); - - assert.ok(segmentStub.called, 'Segment should receive metrics'); - - const firstSegmentEvent = segmentStub.getCall(0).args[0]; - assert.equal(firstSegmentEvent.name, 'Home'); - assert.equal(firstSegmentEvent.context.page.path, '/'); - - const secondSegmentEvent = segmentStub.getCall(1).args[0]; - assert.equal(secondSegmentEvent.name, 'Unlock Page'); - assert.equal(secondSegmentEvent.context.page.path, '/unlock'); - - const thirdSegmentEvent = segmentStub.getCall(2).args[0]; - assert.equal(thirdSegmentEvent.name, 'Home'); - assert.equal(thirdSegmentEvent.context.page.path, '/'); - }, - ); - }); -}); diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js new file mode 100644 index 000000000..fa0d0b30f --- /dev/null +++ b/test/e2e/mock-e2e.js @@ -0,0 +1,63 @@ +async function setupMocking(server, testSpecificMock) { + await server.forAnyRequest().thenPassThrough(); + + await server + .forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices') + .thenCallback(() => { + return { + statusCode: 200, + json: { + SafeGasPrice: '1', + ProposeGasPrice: '2', + FastGasPrice: '3', + }, + }; + }); + + await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => { + return { + statusCode: 200, + }; + }); + + await server + .forGet( + 'https://gas-api.metaswap.codefi.network/networks/1/suggestedGasFees', + ) + .thenCallback(() => { + return { + statusCode: 200, + json: { + low: { + suggestedMaxPriorityFeePerGas: '1', + suggestedMaxFeePerGas: '20.44436136', + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 30000, + }, + medium: { + suggestedMaxPriorityFeePerGas: '1.5', + suggestedMaxFeePerGas: '25.80554517', + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 45000, + }, + high: { + suggestedMaxPriorityFeePerGas: '2', + suggestedMaxFeePerGas: '27.277766977', + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + }, + estimatedBaseFee: '19.444436136', + networkCongestion: 0.14685, + latestPriorityFeeRange: ['0.378818859', '6.555563864'], + historicalPriorityFeeRange: ['0.1', '248.262969261'], + historicalBaseFeeRange: ['14.146999781', '28.825256275'], + priorityFeeTrend: 'down', + baseFeeTrend: 'up', + }, + }; + }); + + testSpecificMock(server); +} + +module.exports = { setupMocking }; diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index e358660d0..2557e5b3f 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,217 @@ 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" + await driver.waitForSelector('.qr-code__address'); + 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', + ); + await driver.waitForSelector('.qr-code__address'); + 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.pasteIntoField( + '[data-testid="import-srp__srp-word-0"]', + 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" + await driver.waitForSelector('.qr-code__address'); + 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" + await driver.waitForSelector('.qr-code__address'); + 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/edit-gas-fee.spec.js b/test/e2e/tests/edit-gas-fee.spec.js index f6898344b..6b79fcbd8 100644 --- a/test/e2e/tests/edit-gas-fee.spec.js +++ b/test/e2e/tests/edit-gas-fee.spec.js @@ -40,9 +40,11 @@ describe('Editing Confirm Transaction', function () { // update estimates to high await driver.clickElement('[data-testid="edit-gas-fee-button"]'); - await driver.delay(regularDelayMs); + await driver.waitForSelector({ + text: 'sec', + tag: 'span', + }); await driver.clickElement('[data-testid="edit-gas-fee-item-high"]'); - await driver.delay(regularDelayMs); await driver.waitForSelector({ text: '🦍' }); await driver.waitForSelector({ text: 'Aggressive', @@ -50,9 +52,7 @@ describe('Editing Confirm Transaction', function () { // update estimates to medium await driver.clickElement('[data-testid="edit-gas-fee-button"]'); - await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="edit-gas-fee-item-medium"]'); - await driver.delay(regularDelayMs); await driver.waitForSelector({ text: '🦊' }); await driver.waitForSelector({ text: 'Market', @@ -60,9 +60,7 @@ describe('Editing Confirm Transaction', function () { // update estimates to low await driver.clickElement('[data-testid="edit-gas-fee-button"]'); - await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="edit-gas-fee-item-low"]'); - await driver.delay(regularDelayMs); await driver.waitForSelector({ text: '🐢' }); await driver.waitForSelector({ text: 'Low', @@ -121,7 +119,10 @@ describe('Editing Confirm Transaction', function () { // update estimates to high await driver.clickElement('[data-testid="edit-gas-fee-button"]'); - await driver.delay(regularDelayMs); + await driver.waitForSelector({ + text: 'sec', + tag: 'span', + }); await driver.clickElement('[data-testid="edit-gas-fee-item-custom"]'); await driver.delay(regularDelayMs); @@ -231,7 +232,10 @@ describe('Editing Confirm Transaction', function () { }); await driver.clickElement('[data-testid="edit-gas-fee-button"]'); - await driver.delay(regularDelayMs); + await driver.waitForSelector({ + text: 'sec', + tag: 'span', + }); await driver.clickElement( '[data-testid="edit-gas-fee-item-dappSuggested"]', ); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index 415a53319..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.pasteIntoField( - '[data-testid="import-srp__srp-word-0"]', - 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 e08a6a30c..8daa2024f 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.pasteIntoField( diff --git a/test/e2e/tests/metrics.spec.js b/test/e2e/tests/metrics.spec.js new file mode 100644 index 000000000..e7bfe3842 --- /dev/null +++ b/test/e2e/tests/metrics.spec.js @@ -0,0 +1,61 @@ +const { strict: assert } = require('assert'); +const { convertToHexValue, withFixtures } = require('../helpers'); + +describe('Segment metrics', function () { + async function mockSegment(mockServer) { + mockServer.reset(); + await mockServer.forAnyRequest().thenPassThrough(); + return await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ batch: [{ type: 'page' }] }) + .times(3) + .thenCallback(() => { + return { + statusCode: 200, + }; + }); + } + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + it('should send first three Page metric events upon fullscreen page load', async function () { + await withFixtures( + { + fixtures: 'metrics-enabled', + ganacheOptions, + title: this.test.title, + }, + async ({ driver, mockServer }) => { + const mockedEndpoints = await mockSegment(mockServer); + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + await driver.wait(async () => { + const isPending = await mockedEndpoints.isPending(); + return isPending === false; + }, 10000); + const mockedRequests = await mockedEndpoints.getSeenRequests(); + assert.equal(mockedRequests.length, 3); + const [firstMock, secondMock, thirdMock] = mockedRequests; + let [mockJson] = firstMock.body.json.batch; + let { title, path } = mockJson.context.page; + assert.equal(title, 'Home'); + assert.equal(path, '/'); + [mockJson] = secondMock.body.json.batch; + ({ title, path } = mockJson.context.page); + assert.equal(title, 'Unlock Page'); + assert.equal(path, '/unlock'); + [mockJson] = thirdMock.body.json.batch; + ({ title, path } = mockJson.context.page); + assert.equal(title, 'Home'); + assert.equal(path, '/'); + }, + ); + }); +}); diff --git a/test/e2e/tests/phishing-detection.spec.js b/test/e2e/tests/phishing-detection.spec.js new file mode 100644 index 000000000..1e6095343 --- /dev/null +++ b/test/e2e/tests/phishing-detection.spec.js @@ -0,0 +1,53 @@ +/* eslint-disable mocha/no-skipped-tests */ +const { strict: assert } = require('assert'); +const { convertToHexValue, withFixtures } = require('../helpers'); + +describe('Phishing Detection', function () { + async function mockPhishingDetection(mockServer) { + await mockServer + .forGet( + 'https://cdn.jsdelivr.net/gh/MetaMask/eth-phishing-detect@master/src/config.json', + ) + .thenCallback(() => { + return { + statusCode: 200, + json: { + version: 2, + tolerance: 2, + fuzzylist: [], + whitelist: [], + blacklist: ['example.com'], + }, + }; + }); + } + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + it.skip('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/e2e/tests/token-details.spec.js b/test/e2e/tests/token-details.spec.js new file mode 100644 index 000000000..6b182fc03 --- /dev/null +++ b/test/e2e/tests/token-details.spec.js @@ -0,0 +1,50 @@ +const { strict: assert } = require('assert'); +const { convertToHexValue, withFixtures } = require('../helpers'); + +describe('Token Details', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + it('should show token details for an imported token', async function () { + 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.clickElement({ text: 'import tokens', tag: 'a' }); + await driver.clickElement({ text: 'Custom Token', tag: 'button' }); + + const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; + const tokenSymbol = 'AAVE'; + + await driver.fill('#custom-address', tokenAddress); + await driver.waitForSelector('#custom-symbol-helper-text'); + await driver.fill('#custom-symbol', tokenSymbol); + await driver.clickElement({ text: 'Add Custom Token', tag: 'button' }); + await driver.clickElement({ text: 'Import Tokens', tag: 'button' }); + await driver.clickElement('[title="Asset options"]'); + await driver.clickElement({ text: 'Token details', tag: 'span' }); + + const tokenAddressFound = { + text: tokenAddress, + }; + + const exists = await driver.isElementPresent(tokenAddressFound); + + assert.ok(exists, 'Token details are not correct.'); + }, + ); + }); +}); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 89062263f..8f23f3cdc 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -248,6 +248,15 @@ class Driver { assert.ok(!dataTab, 'Found element that should not be present'); } + async isElementPresent(element) { + try { + await this.findElement(element); + return true; + } catch (err) { + return false; + } + } + /** * Paste a string into a field. * 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/index.scss b/ui/components/app/account-menu/index.scss index 96d43e2bc..8abdc9207 100644 --- a/ui/components/app/account-menu/index.scss +++ b/ui/components/app/account-menu/index.scss @@ -108,11 +108,11 @@ width: 59px; &:hover { - background-color: rgba($dusty-gray, 0.05); + background-color: rgba(#9b9b9b, 0.05); } &:active { - background-color: rgba($dusty-gray, 0.1); + background-color: rgba(#9b9b9b, 0.1); } } 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..0bc911fed --- /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'; +import IconCaretLeft from '../../ui/icon/icon-caret-left'; + +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) => ( + + {t('logo', + + {item.ticker} + + {`${t('add')} + + ))} + + + + + {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..9a087b259 --- /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 { + margin-left: 24px; + margin-right: 16px; + } + } + + &__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-defaults/advanced-gas-fee-defaults.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js index cd21dd340..beefff4fa 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js @@ -88,7 +88,11 @@ const AdvancedGasFeeDefaults = () => { onClick={handleUpdateDefaultSettings} disabled={gasErrors.maxFeePerGas || gasErrors.maxPriorityFeePerGas} /> - + {isDefaultSettingsSelected ? t('advancedGasFeeDefaultOptOut') : t('advancedGasFeeDefaultOptIn', [ diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js index 91df48fde..51739f704 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js @@ -1,42 +1,95 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; +import { isNullish } from '../../../../helpers/utils/util'; +import { formatGasFeeOrFeeRange } from '../../../../helpers/utils/gas'; +import { I18nContext } from '../../../../contexts/i18n'; import Box from '../../../ui/box'; -import I18nValue from '../../../ui/i18n-value'; import LoadingHeartBeat from '../../../ui/loading-heartbeat'; -const AdvancedGasFeeInputSubtext = ({ latest, historical, feeTrend }) => { +function determineTrendInfo(trend, t) { + switch (trend) { + case 'up': + return { + className: 'advanced-gas-fee-input-subtext__up', + imageSrc: '/images/up-arrow.svg', + imageAlt: t('upArrow'), + }; + case 'down': + return { + className: 'advanced-gas-fee-input-subtext__down', + imageSrc: '/images/down-arrow.svg', + imageAlt: t('downArrow'), + }; + case 'level': + return { + className: 'advanced-gas-fee-input-subtext__level', + imageSrc: '/images/level-arrow.svg', + imageAlt: t('levelArrow'), + }; + default: + return null; + } +} + +const AdvancedGasFeeInputSubtext = ({ latest, historical, trend }) => { + const t = useContext(I18nContext); + const trendInfo = determineTrendInfo(trend, t); return ( - - - - - - - - {latest} - - - feeTrend-arrow - - - - - - - - - {historical} - - + + {isNullish(latest) ? null : ( + + + {t('currentTitle')} + + + + {formatGasFeeOrFeeRange(latest)} + + {trendInfo === null ? null : ( + + {trendInfo.imageAlt} + + )} + + )} + {isNullish(historical) ? null : ( + + + {t('twelveHrTitle')} + + + + {formatGasFeeOrFeeRange(historical)} + + + )} ); }; AdvancedGasFeeInputSubtext.propTypes = { - latest: PropTypes.string, - historical: PropTypes.string, - feeTrend: PropTypes.string.isRequired, + latest: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), + historical: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), + trend: PropTypes.oneOf(['up', 'down', 'level']), }; export default AdvancedGasFeeInputSubtext; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js index ab3692de3..5d779b669 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js @@ -1,55 +1,144 @@ import React from 'react'; -import { screen } from '@testing-library/react'; - -import { GAS_ESTIMATE_TYPES } from '../../../../../shared/constants/gas'; -import mockEstimates from '../../../../../test/data/mock-estimates.json'; -import mockState from '../../../../../test/data/mock-state.json'; -import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import { renderWithProvider, screen } from '../../../../../test/jest'; import configureStore from '../../../../store/store'; import AdvancedGasFeeInputSubtext from './advanced-gas-fee-input-subtext'; jest.mock('../../../../store/actions', () => ({ disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest - .fn() - .mockImplementation(() => Promise.resolve()), + getGasFeeEstimatesAndStartPolling: jest.fn().mockResolvedValue(null), addPollingTokenToAppState: jest.fn(), removePollingTokenFromAppState: jest.fn(), })); -const render = () => { - const store = configureStore({ - metamask: { - ...mockState.metamask, - accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, - balance: '0x1F4', - }, - }, - advancedGasFee: { priorityFee: 100 }, - featureFlags: { advancedInlineGas: true }, - gasFeeEstimates: - mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates, - }, - }); - - return renderWithProvider( - , - store, - ); +const renderComponent = ({ props = {}, state = {} } = {}) => { + const store = configureStore(state); + return renderWithProvider(, store); }; describe('AdvancedGasFeeInputSubtext', () => { - it('should renders latest and historical values passed', () => { - render(); + describe('when "latest" is non-nullish', () => { + it('should render the latest fee if given a fee', () => { + renderComponent({ + props: { + latest: '123.12345', + }, + }); - expect(screen.queryByText('Latest Value')).toBeInTheDocument(); - expect(screen.queryByText('Historical value')).toBeInTheDocument(); - expect(screen.queryByAltText('feeTrend-arrow')).toBeInTheDocument(); + expect(screen.getByText('123.12 GWEI')).toBeInTheDocument(); + }); + + it('should render the latest fee range if given a fee range', () => { + renderComponent({ + props: { + latest: ['123.456', '456.789'], + }, + }); + + expect(screen.getByText('123.46 - 456.79 GWEI')).toBeInTheDocument(); + }); + + it('should render a fee trend arrow image if given "up" as the trend', () => { + renderComponent({ + props: { + latest: '123.12345', + trend: 'up', + }, + }); + + expect(screen.getByAltText('up arrow')).toBeInTheDocument(); + }); + + it('should render a fee trend arrow image if given "down" as the trend', () => { + renderComponent({ + props: { + latest: '123.12345', + trend: 'down', + }, + }); + + expect(screen.getByAltText('down arrow')).toBeInTheDocument(); + }); + + it('should render a fee trend arrow image if given "level" as the trend', () => { + renderComponent({ + props: { + latest: '123.12345', + trend: 'level', + }, + }); + + expect(screen.getByAltText('level arrow')).toBeInTheDocument(); + }); + + it('should not render a fee trend arrow image if given an invalid trend', () => { + // Suppress warning from PropTypes, which we expect + jest.spyOn(console, 'error').mockImplementation(); + + renderComponent({ + props: { + latest: '123.12345', + trend: 'whatever', + }, + }); + + expect(screen.queryByTestId('fee-arrow')).not.toBeInTheDocument(); + }); + + it('should not render a fee trend arrow image if given a nullish trend', () => { + renderComponent({ + props: { + latest: '123.12345', + trend: null, + }, + }); + + expect(screen.queryByTestId('fee-arrow')).not.toBeInTheDocument(); + }); + }); + + describe('when "latest" is nullish', () => { + it('should not render the container for the latest fee', () => { + renderComponent({ + props: { + latest: null, + }, + }); + + expect(screen.queryByTestId('latest')).not.toBeInTheDocument(); + }); + }); + + describe('when "historical" is not nullish', () => { + it('should render the historical fee if given a fee', () => { + renderComponent({ + props: { + historical: '123.12345', + }, + }); + + expect(screen.getByText('123.12 GWEI')).toBeInTheDocument(); + }); + + it('should render the historical fee range if given a fee range', () => { + renderComponent({ + props: { + historical: ['123.456', '456.789'], + }, + }); + + expect(screen.getByText('123.46 - 456.79 GWEI')).toBeInTheDocument(); + }); + }); + + describe('when "historical" is nullish', () => { + it('should not render the container for the historical fee', () => { + renderComponent({ + props: { + historical: null, + }, + }); + + expect(screen.queryByTestId('historical')).not.toBeInTheDocument(); + }); }); }); diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss index 7a3f06509..c78f2c999 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss @@ -1,6 +1,4 @@ .advanced-gas-fee-input-subtext { - display: flex; - align-items: center; margin-top: 2px; color: var(--ui-4); font-size: $font-size-h8; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js index 00b7cc30b..ee3737431 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js @@ -6,12 +6,8 @@ import { EDIT_GAS_MODES, PRIORITY_LEVELS, } from '../../../../../../shared/constants/gas'; -import { SECONDARY } from '../../../../../helpers/constants/common'; -import { - bnGreaterThan, - bnLessThan, - roundToDecimalPlacesRemovingExtraZeroes, -} from '../../../../../helpers/utils/util'; +import { PRIMARY } from '../../../../../helpers/constants/common'; +import { bnGreaterThan, bnLessThan } from '../../../../../helpers/utils/util'; import { decGWEIToHexWEI } from '../../../../../helpers/utils/conversions.util'; import { getAdvancedGasFeeValues } from '../../../../../selectors'; import { useGasFeeContext } from '../../../../../contexts/gasFee'; @@ -23,7 +19,6 @@ import FormField from '../../../../ui/form-field'; import { useAdvancedGasFeePopoverContext } from '../../context'; import AdvancedGasFeeInputSubtext from '../../advanced-gas-fee-input-subtext'; -import { renderFeeRange } from '../utils'; const validateBaseFee = (value, gasFeeEstimates, maxPriorityFeePerGas) => { if (bnGreaterThan(maxPriorityFeePerGas, value)) { @@ -57,6 +52,7 @@ const BaseFeeInput = () => { editGasMode, } = useGasFeeContext(); const { + gasLimit, maxPriorityFeePerGas, setErrorValue, setMaxFeePerGas, @@ -69,7 +65,7 @@ const BaseFeeInput = () => { baseFeeTrend, } = gasFeeEstimates; const [baseFeeError, setBaseFeeError] = useState(); - const { currency, numberOfDecimals } = useUserPreferencedCurrency(SECONDARY); + const { currency, numberOfDecimals } = useUserPreferencedCurrency(PRIMARY); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); @@ -85,8 +81,8 @@ const BaseFeeInput = () => { return maxFeePerGas; }); - const [, { value: baseFeeInFiat }] = useCurrencyDisplay( - decGWEIToHexWEI(baseFee), + const [baseFeeInPrimaryCurrency] = useCurrencyDisplay( + decGWEIToHexWEI(baseFee * gasLimit), { currency, numberOfDecimals }, ); @@ -128,16 +124,13 @@ const BaseFeeInput = () => { titleUnit={`(${t('gwei')})`} tooltipText={t('advancedBaseGasFeeToolTip')} value={baseFee} - detailText={`≈ ${baseFeeInFiat}`} + detailText={`≈ ${baseFeeInPrimaryCurrency}`} numeric /> ); 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..dbec8b725 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 @@ -12,6 +12,7 @@ import { GasFeeContextProvider } from '../../../../../contexts/gasFee'; import configureStore from '../../../../../store/store'; import { AdvancedGasFeePopoverContextProvider } from '../../context'; +import AdvancedGasFeeGasLimit from '../../advanced-gas-fee-gas-limit'; import BaseFeeInput from './base-fee-input'; jest.mock('../../../../../store/actions', () => ({ @@ -50,6 +51,7 @@ const render = (txProps, contextProps) => { > + , store, @@ -60,24 +62,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', () => { @@ -88,22 +90,27 @@ describe('BaseFeeInput', () => { }); expect(document.getElementsByTagName('input')[0]).toHaveValue(200); }); - it('should show current value of estimatedBaseFee in subtext', () => { + + it('should show current value of estimatedBaseFee in users primary currency in right side of input box', () => { render({ txParams: { - maxFeePerGas: '0x174876E800', + gas: '0x5208', + maxFeePerGas: '0x2E90EDD000', }, }); + expect(screen.queryByText('≈ 0.0042 ETH')).toBeInTheDocument(); + }); + + it('should show current value of estimatedBaseFee in subtext', () => { + 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', () => { render({ txParams: { diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js index de223da10..a2b13fe94 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js @@ -6,7 +6,7 @@ import { EDIT_GAS_MODES, PRIORITY_LEVELS, } from '../../../../../../shared/constants/gas'; -import { SECONDARY } from '../../../../../helpers/constants/common'; +import { PRIMARY } from '../../../../../helpers/constants/common'; import { decGWEIToHexWEI } from '../../../../../helpers/utils/conversions.util'; import { getAdvancedGasFeeValues } from '../../../../../selectors'; import { useCurrencyDisplay } from '../../../../../hooks/useCurrencyDisplay'; @@ -19,7 +19,6 @@ import { bnGreaterThan, bnLessThan } from '../../../../../helpers/utils/util'; import { useAdvancedGasFeePopoverContext } from '../../context'; import AdvancedGasFeeInputSubtext from '../../advanced-gas-fee-input-subtext'; -import { renderFeeRange } from '../utils'; const validatePriorityFee = (value, gasFeeEstimates) => { if (value <= 0) { @@ -48,6 +47,7 @@ const PriorityFeeInput = () => { const t = useI18nContext(); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); const { + gasLimit, setErrorValue, setMaxPriorityFeePerGas, } = useAdvancedGasFeePopoverContext(); @@ -75,10 +75,10 @@ const PriorityFeeInput = () => { return maxPriorityFeePerGas; }); - const { currency, numberOfDecimals } = useUserPreferencedCurrency(SECONDARY); + const { currency, numberOfDecimals } = useUserPreferencedCurrency(PRIMARY); - const [, { value: priorityFeeInFiat }] = useCurrencyDisplay( - decGWEIToHexWEI(priorityFee), + const [priorityFeeInPrimaryCurrency] = useCurrencyDisplay( + decGWEIToHexWEI(priorityFee * gasLimit), { currency, numberOfDecimals }, ); @@ -112,13 +112,13 @@ const PriorityFeeInput = () => { titleUnit={`(${t('gwei')})`} tooltipText={t('advancedPriorityFeeToolTip')} value={priorityFee} - detailText={`≈ ${priorityFeeInFiat}`} + detailText={`≈ ${priorityFeeInPrimaryCurrency}`} numeric /> ); 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..31ceb6821 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 @@ -12,6 +12,7 @@ import { GasFeeContextProvider } from '../../../../../contexts/gasFee'; import configureStore from '../../../../../store/store'; import { AdvancedGasFeePopoverContextProvider } from '../../context'; +import AdvancedGasFeeGasLimit from '../../advanced-gas-fee-gas-limit'; import PriorityfeeInput from './priority-fee-input'; jest.mock('../../../../../store/actions', () => ({ @@ -50,6 +51,7 @@ const render = (txProps, contextProps) => { > + , store, @@ -60,24 +62,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', () => { @@ -88,22 +90,27 @@ 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', () => { + + it('should show current value of priority fee in users primary currency in right side of input box', () => { render({ txParams: { - maxFeePerGas: '0x174876E800', + gas: '0x5208', + maxPriorityFeePerGas: '0x77359400', }, }); + expect(screen.queryByText('≈ 0.000042 ETH')).toBeInTheDocument(); + }); + + it('should show 12hr range value in subtext', () => { + render(); expect(screen.queryByText('2 - 125 GWEI')).toBeInTheDocument(); }); + it('should show error if value entered is 0', () => { render({ txParams: { diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/utils.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/utils.js deleted file mode 100644 index 8782628f8..000000000 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/utils.js +++ /dev/null @@ -1,13 +0,0 @@ -import { uniq } from 'lodash'; - -import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; - -export const renderFeeRange = (feeRange) => { - if (feeRange) { - const formattedRange = uniq( - feeRange.map((fee) => roundToDecimalPlacesRemovingExtraZeroes(fee, 2)), - ).join(' - '); - return `${formattedRange} GWEI`; - } - return null; -}; diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 2eb88d5dd..664aa9523 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'; diff --git a/ui/components/app/app-header/app-header.component.js b/ui/components/app/app-header/app-header.component.js index 1694362c6..0b50e3333 100644 --- a/ui/components/app/app-header/app-header.component.js +++ b/ui/components/app/app-header/app-header.component.js @@ -122,9 +122,6 @@ export default class AppHeader extends PureComponent { {!hideNetworkIndicator && (
this.handleNetworkIndicatorClick(event)} disabled={disabled || disableNetworkIndicator} /> diff --git a/ui/components/app/app-header/index.scss b/ui/components/app/app-header/index.scss index 1cdd03b28..0e7c26f8e 100644 --- a/ui/components/app/app-header/index.scss +++ b/ui/components/app/app-header/index.scss @@ -97,11 +97,4 @@ width: 0; justify-content: flex-end; } - - &__network-down-arrow { - background-image: url(/images/icons/caret-down.svg); - background-repeat: no-repeat; - background-size: contain; - background-position: center; - } } diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 193b22619..c0b1347d0 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -100,7 +100,7 @@ const AssetList = ({ onClickAsset }) => { diff --git a/ui/components/app/collectible-details/collectible-details.js b/ui/components/app/collectible-details/collectible-details.js index 195953545..be538d852 100644 --- a/ui/components/app/collectible-details/collectible-details.js +++ b/ui/components/app/collectible-details/collectible-details.js @@ -18,11 +18,7 @@ import { BLOCK_SIZES, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { - getAssetImageURL, - isEqualCaseInsensitive, - shortenAddress, -} from '../../../helpers/utils/util'; +import { getAssetImageURL, shortenAddress } from '../../../helpers/utils/util'; import { getCurrentChainId, getIpfsGateway, @@ -54,6 +50,7 @@ import InfoTooltip from '../../ui/info-tooltip'; import { ERC721 } from '../../../helpers/constants/common'; import { usePrevious } from '../../../hooks/usePrevious'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; export default function CollectibleDetails({ collectible }) { const { @@ -187,7 +184,7 @@ export default function CollectibleDetails({ collectible }) { >
@@ -229,7 +226,7 @@ export default function CollectibleDetails({ collectible }) { diff --git a/ui/components/app/collectibles-tab/collectibles-tab.js b/ui/components/app/collectibles-tab/collectibles-tab.js index 5f2703dd2..4252ba1f0 100644 --- a/ui/components/app/collectibles-tab/collectibles-tab.js +++ b/ui/components/app/collectibles-tab/collectibles-tab.js @@ -83,7 +83,7 @@ export default function CollectiblesTab({ onAddNFT }) { className="collectibles-tab__link" > @@ -134,7 +134,7 @@ export default function CollectiblesTab({ onAddNFT }) { )} 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('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..7da4ddeba 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,18 @@ /* 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'; +import Typography from '../../../../ui/typography'; +import { TYPOGRAPHY } from '../../../../../helpers/constants/design-system'; const ConfirmPageContainerSummary = (props) => { const { @@ -12,13 +22,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 +66,12 @@ const ConfirmPageContainerSummary = (props) => { src={image} /> ); - } else if (identiconAddress) { + } else if (contractAddress) { return ( ); @@ -47,7 +85,29 @@ const ConfirmPageContainerSummary = (props) => {
{origin}
)}
-
{action}
+
+ {isContractTypeTransaction && toName && ( + + + : + + )} + + {action} + + {isContractTypeTransaction && isTrusted === false && ( + + )} +
{nonce && (
{`#${nonce}`} @@ -58,9 +118,15 @@ const ConfirmPageContainerSummary = (props) => {
{renderImage()} {!hideTitle ? ( -
+ {titleComponent || title} -
+ ) : null}
{hideSubtitle ? null : ( @@ -69,6 +135,12 @@ const ConfirmPageContainerSummary = (props) => {
)} + {showNicknamePopovers && ( + setShowNicknamePopovers(false)} + address={checksummedAddress} + /> + )}
); }; @@ -81,10 +153,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..53fb24a22 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 { @@ -53,6 +77,14 @@ text-overflow: ellipsis; } + &__title-text-long { + @include H3; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + &__subtitle { @include H5; 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-header/confirm-page-container-header.component.js b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index 2d1ff99b0..eb33888e1 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -7,6 +7,7 @@ import { import { getEnvironmentType } from '../../../../../app/scripts/lib/util'; import NetworkDisplay from '../../network-display'; import Identicon from '../../../ui/identicon'; +import IconCaretLeft from '../../../ui/icon/icon-caret-left'; import { shortenAddress } from '../../../../helpers/utils/util'; import AccountMismatchWarning from '../../../ui/account-mismatch-warning/account-mismatch-warning.component'; import { useI18nContext } from '../../../../hooks/useI18nContext'; @@ -47,7 +48,7 @@ export default function ConfirmPageContainerHeader({ visibility: showEdit ? 'initial' : 'hidden', }} > - + onEdit()} diff --git a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.stories.js b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.stories.js new file mode 100644 index 000000000..2c8196b3b --- /dev/null +++ b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.stories.js @@ -0,0 +1,35 @@ +import React from 'react'; +import ConfirmPageContainerHeader from '.'; + +export default { + title: 'Components/App/ConfirmPageContainer/ConfirmPageContainerHeader', + id: __filename, + argTypes: { + accountAddress: { + control: 'text', + }, + showAccountInHeader: { + control: 'boolean', + }, + showEdit: { + control: 'boolean', + }, + onEdit: { + action: 'onEdit', + }, + children: { + control: 'text', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + showEdit: false, + showAccountInHeader: false, + accountAddress: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', + children: 'children', +}; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-header/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-header/index.scss index 0ad152205..1ffb8707a 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-header/index.scss +++ b/ui/components/app/confirm-page-container/confirm-page-container-header/index.scss @@ -21,6 +21,7 @@ display: flex; justify-content: center; align-items: center; + color: var(--color-primary-default); [dir='rtl'] & img { transform: rotate(180deg); @@ -30,7 +31,6 @@ &__back-button { @include Paragraph; - color: #2f9ae0; cursor: pointer; padding-left: 5px; } 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('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/enableEIP1559V2-notice/enableEIP1559V2-notice.js b/ui/components/app/confirm-page-container/enableEIP1559V2-notice/enableEIP1559V2-notice.js index f0b92ff03..b9019424e 100644 --- a/ui/components/app/confirm-page-container/enableEIP1559V2-notice/enableEIP1559V2-notice.js +++ b/ui/components/app/confirm-page-container/enableEIP1559V2-notice/enableEIP1559V2-notice.js @@ -50,7 +50,7 @@ export default function EnableEIP1559V2Notice({ isFirstAlert }) { { - await onPreferenceToggle(!featureSecondary); + await onPreferenceToggle(); setSwapped(!isSwapped); }; diff --git a/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js b/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js index 32d655168..0433f9a10 100644 --- a/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js +++ b/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js @@ -14,37 +14,53 @@ export default function EditGasDisplayEducation() { return (
- + {t('editGasEducationModalIntro')} {t('editGasHigh')} - + {t('editGasEducationHighExplanation')} {t('editGasMedium')} - + {t('editGasEducationMediumExplanation')} {t('editGasLow')} - + {t('editGasEducationLowExplanation')}
diff --git a/ui/components/app/edit-gas-display/edit-gas-display.component.js b/ui/components/app/edit-gas-display/edit-gas-display.component.js index 4e0aa83cb..f7fc433c9 100644 --- a/ui/components/app/edit-gas-display/edit-gas-display.component.js +++ b/ui/components/app/edit-gas-display/edit-gas-display.component.js @@ -182,7 +182,7 @@ export default function EditGasDisplay({ {mode === EDIT_GAS_MODES.SPEED_UP && (
diff --git a/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.js b/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.js index b3610c2b6..7f73181c9 100644 --- a/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.js +++ b/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.js @@ -80,7 +80,10 @@ export default function EditGasFeeButton({ userAcknowledgedGasMissing }) { - + {t('dappSuggestedTooltip', [transaction.origin])} diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js index cb11ed53b..850eb17ed 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.js @@ -91,7 +91,7 @@ const EditGasFeePopover = () => { diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss b/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss index 86acf50ee..c9bcf1952 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/index.scss @@ -83,6 +83,6 @@ &__time-estimate-medium, &__time-estimate-high { - color: var(--success-3); + color: var(--color-success-default); } } diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.js b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.js index 0447513a2..b049a1bc4 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.js @@ -124,7 +124,7 @@ const EditGasToolTip = ({ {maxFeePerGas && ( {roundToDecimalPlacesRemovingExtraZeroes(maxFeePerGas, 4)} @@ -142,7 +142,7 @@ const EditGasToolTip = ({ {maxPriorityFeePerGas && ( {roundToDecimalPlacesRemovingExtraZeroes( @@ -163,7 +163,7 @@ const EditGasToolTip = ({ {gasLimit && ( {roundToDecimalPlacesRemovingExtraZeroes(gasLimit, 4)} diff --git a/ui/components/app/edit-gas-fee-popover/network-statistics/index.scss b/ui/components/app/edit-gas-fee-popover/network-statistics/index.scss index d52741eb9..3204fbf14 100644 --- a/ui/components/app/edit-gas-fee-popover/network-statistics/index.scss +++ b/ui/components/app/edit-gas-fee-popover/network-statistics/index.scss @@ -7,37 +7,32 @@ height: 56px; display: flex; align-items: center; - justify-content: space-around; + justify-content: center; + } - &__separator { - border-left: 1px solid var(--ui-2); - height: 65%; + &__field { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + + &:not(:last-child) { + border-right: 1px solid var(--ui-2); } + } - &__field { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 30%; + &__field-data { + color: var(--color-text-alternative); + font-size: 12px; + text-align: center; + } - &-data { - color: var(--ui-4); - font-size: 12px; - text-align: center; - } - - &-label { - color: var(--Black-100); - font-size: 10px; - font-weight: bold; - margin-top: 4px; - } - } - - .latest-priority-fee-field { - width: 40%; - } + &__field-label { + color: var(--color-text-default); + font-size: 10px; + font-weight: bold; + margin-top: 4px; } &__tooltip-label { diff --git a/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/index.js b/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/index.js deleted file mode 100644 index f1bc7464f..000000000 --- a/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './latest-priority-fee-field'; diff --git a/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.js b/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.js deleted file mode 100644 index 12585123c..000000000 --- a/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useMemo } from 'react'; -import { uniq } from 'lodash'; -import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../../helpers/utils/util'; -import { useGasFeeContext } from '../../../../../contexts/gasFee'; -import I18nValue from '../../../../ui/i18n-value'; -import { PriorityFeeTooltip } from '../tooltips'; - -export default function LatestPriorityFeeField() { - const { gasFeeEstimates } = useGasFeeContext(); - - const priorityFeeRange = useMemo(() => { - const { latestPriorityFeeRange } = gasFeeEstimates; - if (latestPriorityFeeRange) { - const formattedRange = uniq([ - roundToDecimalPlacesRemovingExtraZeroes(latestPriorityFeeRange[0], 1), - roundToDecimalPlacesRemovingExtraZeroes(latestPriorityFeeRange[1], 0), - ]).join(' - '); - return `${formattedRange} GWEI`; - } - return null; - }, [gasFeeEstimates]); - - return ( -
- - - {priorityFeeRange} - - - - - -
- ); -} diff --git a/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.test.js b/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.test.js deleted file mode 100644 index f9ece5744..000000000 --- a/ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../../../test/jest'; -import { GasFeeContext } from '../../../../../contexts/gasFee'; -import configureStore from '../../../../../store/store'; - -import LatestPriorityFeeField from './latest-priority-fee-field'; - -const renderComponent = (gasFeeEstimates) => { - const store = configureStore({}); - return renderWithProvider( - - - , - store, - ); -}; - -describe('LatestPriorityFeeField', () => { - it('should render a version of latest priority fee range pulled from context, lower range rounded to 1 decimal place', () => { - const { getByText } = renderComponent({ - latestPriorityFeeRange: ['1.000001668', '2.5634234'], - }); - expect(getByText('1 - 3 GWEI')).toBeInTheDocument(); - }); - - it('should render nothing if gasFeeEstimates are empty', () => { - const { queryByText } = renderComponent({}); - expect(queryByText('GWEI')).not.toBeInTheDocument(); - }); -}); diff --git a/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.js b/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.js index 9e2f730c5..548490b2e 100644 --- a/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.js +++ b/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.js @@ -1,53 +1,78 @@ -import React from 'react'; - +import React, { useContext } from 'react'; import { COLORS, FONT_WEIGHT, TYPOGRAPHY, } from '../../../../helpers/constants/design-system'; -import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; +import { isNullish } from '../../../../helpers/utils/util'; +import { formatGasFeeOrFeeRange } from '../../../../helpers/utils/gas'; +import { I18nContext } from '../../../../contexts/i18n'; import { useGasFeeContext } from '../../../../contexts/gasFee'; -import I18nValue from '../../../ui/i18n-value'; import Typography from '../../../ui/typography/typography'; - -import { BaseFeeTooltip } from './tooltips'; -import LatestPriorityFeeField from './latest-priority-fee-field'; +import { BaseFeeTooltip, PriorityFeeTooltip } from './tooltips'; import StatusSlider from './status-slider'; const NetworkStatistics = () => { + const t = useContext(I18nContext); const { gasFeeEstimates } = useGasFeeContext(); + const formattedLatestBaseFee = formatGasFeeOrFeeRange( + gasFeeEstimates?.estimatedBaseFee, + { + precision: 0, + }, + ); + const formattedLatestPriorityFeeRange = formatGasFeeOrFeeRange( + gasFeeEstimates?.latestPriorityFeeRange, + { precision: [1, 0] }, + ); + const networkCongestion = gasFeeEstimates?.networkCongestion; return (
- + {t('networkStatus')}
-
- - - {gasFeeEstimates?.estimatedBaseFee && - `${roundToDecimalPlacesRemovingExtraZeroes( - gasFeeEstimates?.estimatedBaseFee, - 0, - )} GWEI`} - - - - - -
-
- -
-
- -
+ {isNullish(formattedLatestBaseFee) ? null : ( +
+ + + {formattedLatestBaseFee} + + + {t('baseFee')} + + +
+ )} + {isNullish(formattedLatestPriorityFeeRange) ? null : ( +
+ + + {formattedLatestPriorityFeeRange} + + + {t('priorityFee')} + + +
+ )} + {isNullish(networkCongestion) ? null : ( +
+ +
+ )}
); diff --git a/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.test.js b/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.test.js index 8dd36da1e..f5f3f4f99 100644 --- a/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.test.js +++ b/ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.test.js @@ -1,15 +1,13 @@ import React from 'react'; - -import { renderWithProvider } from '../../../../../test/jest'; -import { GasFeeContext } from '../../../../contexts/gasFee'; +import { renderWithProvider, screen } from '../../../../../test/jest'; import configureStore from '../../../../store/store'; - +import { GasFeeContext } from '../../../../contexts/gasFee'; import NetworkStatistics from './network-statistics'; -const renderComponent = (gasFeeEstimates) => { - const store = configureStore({}); +const renderComponent = ({ gasFeeContext = {}, state = {} } = {}) => { + const store = configureStore(state); return renderWithProvider( - + , store, @@ -17,17 +15,104 @@ const renderComponent = (gasFeeEstimates) => { }; describe('NetworkStatistics', () => { - it('should render the latest base fee without decimals', () => { - const { getByText } = renderComponent({ - estimatedBaseFee: '50.0112', + it('should render the latest base fee rounded to no decimal places', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: { + estimatedBaseFee: '50.0112', + }, + }, }); - expect(getByText('50 GWEI')).toBeInTheDocument(); + expect(screen.getByText('50 GWEI')).toBeInTheDocument(); }); - it('should render a version of latest priority fee range pulled from context, lower range rounded to 1 decimal place', () => { - const { getByText } = renderComponent({ - latestPriorityFeeRange: ['1.000001668', '2.5634234'], + it('should not render the latest base fee if it is not present', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: { + estimatedBaseFee: null, + }, + }, }); - expect(getByText('1 - 3 GWEI')).toBeInTheDocument(); + expect( + screen.queryByTestId('formatted-latest-base-fee'), + ).not.toBeInTheDocument(); + }); + + it('should not render the latest base fee if no gas fee estimates are available', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: null, + }, + }); + expect( + screen.queryByTestId('formatted-latest-base-fee'), + ).not.toBeInTheDocument(); + }); + + it('should render the latest priority fee range, with the low end of the range rounded to 1 decimal place and the high end rounded to no decimal places', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: { + latestPriorityFeeRange: ['1.100001668', '2.5634234'], + }, + }, + }); + expect(screen.getByText('1.1 - 3 GWEI')).toBeInTheDocument(); + }); + + it('should not render the latest priority fee range if it is not present', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: { + latestPriorityFeeRange: null, + }, + }, + }); + expect( + screen.queryByTestId('formatted-latest-priority-fee-range'), + ).not.toBeInTheDocument(); + }); + + it('should not render the latest priority fee range if no gas fee estimates are available', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: null, + }, + }); + expect( + screen.queryByTestId('formatted-latest-priority-fee-range'), + ).not.toBeInTheDocument(); + }); + + it('should render the network status slider', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: { + networkCongestion: 0.5, + }, + }, + }); + expect(screen.getByText('Stable')).toBeInTheDocument(); + }); + + it('should not render the network status slider if the network congestion is not available', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: { + networkCongestion: null, + }, + }, + }); + expect(screen.queryByTestId('status-slider-label')).not.toBeInTheDocument(); + }); + + it('should not render the network status slider if no gas fee estimates are available', () => { + renderComponent({ + gasFeeContext: { + gasFeeEstimates: null, + }, + }); + expect(screen.queryByTestId('status-slider-label')).not.toBeInTheDocument(); }); }); 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 51f594a5a..210e38c9b 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/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js b/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js index 9e82ce98a..21f16a640 100644 --- a/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js +++ b/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js @@ -1,23 +1,44 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; import Modal from '../../modal'; import Typography from '../../../ui/typography'; import { TYPOGRAPHY } from '../../../../helpers/constants/design-system'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { ADD_COLLECTIBLE_ROUTE } from '../../../../helpers/constants/routes'; +import { + ADD_COLLECTIBLE_ROUTE, + ASSET_ROUTE, +} from '../../../../helpers/constants/routes'; +import { getCollectibles } from '../../../../ducks/metamask/metamask'; +import { removeToken } from '../../../../store/actions'; +import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => { const history = useHistory(); const t = useI18nContext(); + const dispatch = useDispatch(); + const allCollectibles = useSelector(getCollectibles); + const tokenAddedAsNFT = allCollectibles.find(({ address }) => + isEqualCaseInsensitive(address, tokenAddress), + ); + return ( { - history.push({ - pathname: ADD_COLLECTIBLE_ROUTE, - state: { tokenAddress }, - }); + onSubmit={async () => { + if (tokenAddedAsNFT) { + await dispatch(removeToken(tokenAddress)); + const { tokenId } = tokenAddedAsNFT; + history.push({ + pathname: `${ASSET_ROUTE}/${tokenAddress}/${tokenId}`, + }); + } else { + history.push({ + pathname: ADD_COLLECTIBLE_ROUTE, + state: { tokenAddress }, + }); + } hideModal(); }} submitText={t('yes')} @@ -31,7 +52,9 @@ const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => { marginTop: 2, }} > - {t('convertTokenToNFTDescription')} + {tokenAddedAsNFT + ? t('convertTokenToNFTExistDescription') + : t('convertTokenToNFTDescription')}
diff --git a/ui/components/app/network-display/index.scss b/ui/components/app/network-display/index.scss index 9e0a0df9b..1ca9b410a 100644 --- a/ui/components/app/network-display/index.scss +++ b/ui/components/app/network-display/index.scss @@ -11,34 +11,6 @@ cursor: not-allowed; } - &--colored { - background-color: lighten(rgb(125, 128, 130), 45%); - } - - &--mainnet { - background-color: lighten($blue-lagoon, 68%); - } - - &--ropsten { - background-color: lighten($crimson, 45%); - } - - &--kovan { - background-color: lighten($purple, 65%); - } - - &--rinkeby { - background-color: lighten($tulip-tree, 35%); - } - - &--goerli { - background-color: lighten($dodger-blue, 35%); - } - - &--localhost { - background-color: lighten($blue-lagoon, 68%); - } - &.chip { margin: 0; max-width: 100%; @@ -59,8 +31,6 @@ } &__icon { - height: 8px; - width: 12px; display: block; } diff --git a/ui/components/app/network-display/network-display.js b/ui/components/app/network-display/network-display.js index 935f36c53..a92278ef6 100644 --- a/ui/components/app/network-display/network-display.js +++ b/ui/components/app/network-display/network-display.js @@ -15,13 +15,11 @@ import { TYPOGRAPHY, } from '../../../helpers/constants/design-system'; import Chip from '../../ui/chip/chip'; +import IconCaretDown from '../../ui/icon/icon-caret-down'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { isNetworkLoading } from '../../../selectors'; export default function NetworkDisplay({ - colored, - outline, - iconClassName, indicatorSize, disabled, labelProps, @@ -40,7 +38,7 @@ export default function NetworkDisplay({ return ( } rightIcon={ - iconClassName && ( - - ) + onClick ? ( + + ) : null } label={ networkType === NETWORK_TYPE_RPC @@ -71,9 +69,7 @@ export default function NetworkDisplay({ : t(networkType) } className={classnames('network-display', { - 'network-display--colored': colored, 'network-display--disabled': disabled, - [`network-display--${networkType}`]: colored && networkType, 'network-display--clickable': typeof onClick === 'function', })} labelProps={{ @@ -84,23 +80,37 @@ export default function NetworkDisplay({ ); } NetworkDisplay.propTypes = { - colored: PropTypes.bool, + /** + * The size of the indicator + */ indicatorSize: PropTypes.oneOf(Object.values(SIZES)), + /** + * The label props of the label can use most of the Typography props + */ labelProps: Chip.propTypes.labelProps, + /** + * The target network + */ targetNetwork: PropTypes.shape({ type: PropTypes.oneOf([ - ...Object.values(NETWORK_TYPE_TO_ID_MAP), + ...Object.keys(NETWORK_TYPE_TO_ID_MAP), NETWORK_TYPE_RPC, ]), nickname: PropTypes.string, }), - outline: PropTypes.bool, + /** + * Whether the NetworkDisplay is disabled + */ disabled: PropTypes.bool, - iconClassName: PropTypes.string, + /** + * The onClick event handler of the NetworkDisplay + * if it is not passed it is assumed that the NetworkDisplay + * should not be interactive and removes the caret and changes the border color + * of the NetworkDisplay + */ onClick: PropTypes.func, }; NetworkDisplay.defaultProps = { - colored: true, indicatorSize: SIZES.LG, }; diff --git a/ui/components/app/network-display/network-display.stories.js b/ui/components/app/network-display/network-display.stories.js new file mode 100644 index 000000000..8fc695a55 --- /dev/null +++ b/ui/components/app/network-display/network-display.stories.js @@ -0,0 +1,93 @@ +import React from 'react'; + +import { + NETWORK_TYPE_TO_ID_MAP, + NETWORK_TYPE_RPC, +} from '../../../../shared/constants/network'; +import { SIZES } from '../../../helpers/constants/design-system'; + +import NetworkDisplay from '.'; + +export default { + title: 'Components/App/NetworkDisplay', + id: __filename, + argTypes: { + indicatorSize: { + control: 'select', + options: Object.values(SIZES), + }, + labelProps: { + control: 'object', + }, + targetNetwork: { + control: 'select', + options: [...Object.keys(NETWORK_TYPE_TO_ID_MAP), NETWORK_TYPE_RPC], + }, + disabled: { + control: 'boolean', + }, + onClick: { + action: 'onClick', + description: + 'The onClick event handler of the NetworkDisplay. If it is not passed it is assumed that the NetworkDisplay SHOULD NOT be interactive and removes the caret and changes the border color of the NetworkDisplay to border-muted', + }, + }, + args: { + targetNetwork: 'ropsten', + }, +}; + +export const DefaultStory = (args) => ( + +); + +DefaultStory.storyName = 'Default'; + +export const TargetNetwork = (args) => { + const targetNetworkArr = [ + ...Object.keys(NETWORK_TYPE_TO_ID_MAP), + NETWORK_TYPE_RPC, + ]; + return ( + <> + {Object.values(targetNetworkArr).map((variant) => ( + + ))} + + ); +}; + +export const DisplayOnly = (args) => { + const targetNetworkArr = [ + ...Object.keys(NETWORK_TYPE_TO_ID_MAP), + NETWORK_TYPE_RPC, + ]; + return ( + <> + {Object.values(targetNetworkArr).map((variant) => ( + + ))} + + ); +}; diff --git a/ui/components/app/permissions-connect-header/index.scss b/ui/components/app/permissions-connect-header/index.scss index 2e2ff2463..824f4febc 100644 --- a/ui/components/app/permissions-connect-header/index.scss +++ b/ui/components/app/permissions-connect-header/index.scss @@ -1,8 +1,9 @@ .permissions-connect-header { flex: 0; - width: 92%; + width: 100%; &__icon { + width: 100%; display: flex; flex-direction: column; align-items: center; diff --git a/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js b/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js index 23dd36442..46778932f 100644 --- a/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js +++ b/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js @@ -31,7 +31,7 @@ export default function RecoveryPhraseReminder({ onConfirm, hasBackedUp }) { {t('recoveryPhraseReminderItemOne')} diff --git a/ui/components/app/signature-request/index.scss b/ui/components/app/signature-request/index.scss index de89e8116..b92452e46 100644 --- a/ui/components/app/signature-request/index.scss +++ b/ui/components/app/signature-request/index.scss @@ -17,7 +17,6 @@ flex: 1; .network-display { - padding: 0; justify-content: flex-end; margin-left: auto; } diff --git a/ui/components/app/signature-request/signature-request-header/signature-request-header.component.js b/ui/components/app/signature-request/signature-request-header/signature-request-header.component.js index e853ea427..6b94732d7 100644 --- a/ui/components/app/signature-request/signature-request-header/signature-request-header.component.js +++ b/ui/components/app/signature-request/signature-request-header/signature-request-header.component.js @@ -17,7 +17,7 @@ export default class SignatureRequestHeader extends PureComponent { {fromAccount ? : null}
- +
); diff --git a/ui/components/app/signature-request/signature-request-header/signature-request-header.stories.js b/ui/components/app/signature-request/signature-request-header/signature-request-header.stories.js new file mode 100644 index 000000000..281125a0f --- /dev/null +++ b/ui/components/app/signature-request/signature-request-header/signature-request-header.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import SignatureRequestHeader from '.'; + +export default { + title: 'Components/App/SignatureRequest/SignatureRequestHeader', + id: __filename, +}; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; 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/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/srp-input/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/step-progress-bar/index.scss b/ui/components/app/step-progress-bar/index.scss index 20292690b..3e5b12645 100644 --- a/ui/components/app/step-progress-bar/index.scss +++ b/ui/components/app/step-progress-bar/index.scss @@ -17,7 +17,6 @@ ul.two-steps { font-size: 12px; position: relative; text-align: center; - text-transform: uppercase; color: #7d7d7d; z-index: 2; } @@ -84,7 +83,7 @@ ul.two-steps { } } -.progressbar li.active + li::after { +.progressbar li.complete + li::after { background-color: var(--primary-blue); z-index: -1; diff --git a/ui/components/app/step-progress-bar/step-progress-bar.js b/ui/components/app/step-progress-bar/step-progress-bar.js index 1bc0f542c..88f49a826 100644 --- a/ui/components/app/step-progress-bar/step-progress-bar.js +++ b/ui/components/app/step-progress-bar/step-progress-bar.js @@ -1,6 +1,7 @@ import React from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; +import { capitalize } from 'lodash'; import { useI18nContext } from '../../../hooks/useI18nContext'; import Box from '../../ui/box'; import { BLOCK_SIZES } from '../../../helpers/constants/design-system'; @@ -26,26 +27,26 @@ export function ThreeStepProgressBar({ stage }) {
  • = 1, - complete: stage >= 1, + complete: stage > 1, })} > - {t('createPassword')} + {capitalize(t('createPassword'))}
  • = 2, - complete: stage >= 3, + complete: stage > 3, })} > - {t('secureWallet')} + {capitalize(t('secureWallet'))}
  • = 4, - complete: stage >= 5, + complete: stage > 5, })} > - {t('confirmRecoveryPhrase')} + {capitalize(t('confirmRecoveryPhrase'))}
  • @@ -63,7 +64,7 @@ export function TwoStepProgressBar({ stage }) { complete: stage > 1, })} > - {t('confirmRecoveryPhrase')} + {capitalize(t('confirmRecoveryPhrase'))}
  • 2, })} > - {t('createPassword')} + {capitalize(t('createPassword'))}
  • diff --git a/ui/components/app/tab-bar/index.scss b/ui/components/app/tab-bar/index.scss index 87b898c79..36b575e31 100644 --- a/ui/components/app/tab-bar/index.scss +++ b/ui/components/app/tab-bar/index.scss @@ -3,13 +3,13 @@ flex-direction: column; justify-content: flex-start; - &__tab { @include Paragraph; + color: var(--color-text-default); display: flex; flex-flow: row nowrap; - align-items: baseline; + align-items: center; min-width: 0; flex: 0 0 auto; box-sizing: border-box; @@ -31,7 +31,7 @@ @media screen and (max-width: $break-small) { @include H4; - border-bottom: 1px solid var(--alto); + border-bottom: 1px solid var(--color-border-muted); opacity: 1; } @@ -41,7 +41,6 @@ flex-flow: row wrap; align-items: center; position: relative; - z-index: 201; &__title { @include H4; @@ -64,7 +63,6 @@ } } - &__icon { margin-inline-end: 16px; display: flex; @@ -76,15 +74,8 @@ @media screen and (max-width: $break-small) { display: block; - background-image: url('/images/caret-right.svg'); - width: 8.27px; - height: 13.64px; - background-size: contain; - background-repeat: no-repeat; - background-position: center; margin-inline-start: auto; - margin-top: 16px; - margin-inline-end: 16px; + margin-inline-end: 8px; [dir='rtl'] & { transform: rotate(180deg); diff --git a/ui/components/app/tab-bar/tab-bar.js b/ui/components/app/tab-bar/tab-bar.js index c503a801d..3fe2babab 100644 --- a/ui/components/app/tab-bar/tab-bar.js +++ b/ui/components/app/tab-bar/tab-bar.js @@ -2,6 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import IconCaretRight from '../../ui/icon/icon-caret-right'; + const TabBar = (props) => { const { tabs = [], onSelect, isActive } = props; @@ -19,7 +21,7 @@ const TabBar = (props) => {
    {icon}
    {content}
    -
    + ))}
    diff --git a/ui/components/app/tab-bar/tab-bar.stories.js b/ui/components/app/tab-bar/tab-bar.stories.js new file mode 100644 index 000000000..4f9128955 --- /dev/null +++ b/ui/components/app/tab-bar/tab-bar.stories.js @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import TabBar from '.'; + +export default { + title: 'Components/App/TabBar', + id: __filename, + argTypes: { + isActive: { + action: 'isActive', + }, + tabs: { + control: 'array', + }, + onSelect: { + action: 'onSelect', + }, + }, + args: { + tabs: [ + { + icon: , + content: 'General', + key: 'general', + }, + { + icon: , + content: 'Contacts', + key: 'contacts', + }, + { + icon: , + content: 'Snaps', + key: 'snaps', + }, + + { + icon: , + content: 'SecurityAndPrivacy', + key: 'securityAndPrivacy', + }, + { + icon: , + content: 'Alerts', + key: 'alerts', + }, + { + icon: , + content: 'Networks', + key: 'networks', + }, + { + icon: , + content: 'Experimental', + key: 'experimental', + }, + { + icon: , + content: 'About', + key: 'about', + }, + ], + }, +}; + +export const DefaultStory = (args) => { + const [currentTab, setCurrentTab] = useState(''); + const handleOnSelect = (key) => setCurrentTab(key); + const handleIsActive = (key) => currentTab === key; + return ( + + ); +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/transaction-detail-item/transaction-detail-item.component.js b/ui/components/app/transaction-detail-item/transaction-detail-item.component.js index b27cf87e7..8601185a9 100644 --- a/ui/components/app/transaction-detail-item/transaction-detail-item.component.js +++ b/ui/components/app/transaction-detail-item/transaction-detail-item.component.js @@ -44,12 +44,12 @@ export default function TransactionDetailItem({ })} > {detailText && ( - + {detailText} )} {subTitle}
    ) : ( - + {subTitle} )} diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index 8a21da314..b8ced7a8b 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -13,7 +13,7 @@ import Button from '../../ui/button'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../shared/constants/swaps'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; -import { isEqualCaseInsensitive } from '../../../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; const PAGE_INCREMENT = 10; diff --git a/ui/components/app/transaction-total-banner/transaction-total-banner.component.js b/ui/components/app/transaction-total-banner/transaction-total-banner.component.js index 481f79c16..47961f021 100644 --- a/ui/components/app/transaction-total-banner/transaction-total-banner.component.js +++ b/ui/components/app/transaction-total-banner/transaction-total-banner.component.js @@ -11,12 +11,12 @@ export default function TransactionTotalBanner({ }) { return (
    - + {total} {detail && ( 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/account-list.js b/ui/components/ui/account-list/account-list.js index 876393db5..a005857ae 100644 --- a/ui/components/ui/account-list/account-list.js +++ b/ui/components/ui/account-list/account-list.js @@ -21,7 +21,6 @@ const AccountList = ({ }) => { const t = useI18nContext(); const selectedAccountScrollRef = useRef(null); - useLayoutEffect(() => { selectedAccountScrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }, []); @@ -103,7 +102,7 @@ const AccountList = ({ className="choose-account-list__account__balance" type={PRIMARY} value={balance} - style={{ color: '#6A737D' }} + style={{ color: 'var(--color-text-alternative)' }} suffix={nativeCurrency} />
    diff --git a/ui/components/ui/account-list/account-list.stories.js b/ui/components/ui/account-list/account-list.stories.js new file mode 100644 index 000000000..a6059e565 --- /dev/null +++ b/ui/components/ui/account-list/account-list.stories.js @@ -0,0 +1,67 @@ +import React from 'react'; +import AccountList from '.'; + +export default { + title: 'Components/UI/AccountList', // title should follow the folder structure location of the component. Don't use spaces. + id: __filename, + argTypes: { + accounts: { + control: 'object', + }, + selectNewAccountViaModal: { + action: 'selectNewAccountViaModal', + }, + addressLastConnectedMap: { + control: 'object', + }, + nativeCurrency: { + control: 'text', + }, + selectedAccounts: { + control: 'object', + }, + allAreSelected: { + action: 'allAreSelected', + }, + deselectAll: { + action: 'deselectAll', + }, + selectAll: { + action: 'selectAll', + }, + handleAccountClick: { + action: 'handleAccountClick', + }, + }, + args: { + accounts: [ + { + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + addressLabel: 'Account 1', + lastConnectedDate: 'Feb-22-2022', + balance: '8.7a73149c048545a3fe58', + has: () => { + /** nothing to do */ + }, + }, + ], + selectedAccounts: { + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + addressLabel: 'Account 2', + lastConnectedDate: 'Feb-22-2022', + balance: '8.7a73149c048545a3fe58', + has: () => { + /** nothing to do */ + }, + }, + addressLastConnectedMap: { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': 'Feb-22-2022', + }, + allAreSelected: () => true, + nativeCurrency: 'USD', + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/ui/account-list/index.scss b/ui/components/ui/account-list/index.scss index a523c5341..c00b08e0e 100644 --- a/ui/components/ui/account-list/index.scss +++ b/ui/components/ui/account-list/index.scss @@ -3,6 +3,7 @@ flex-direction: column; width: 100%; align-items: center; + overflow-y: auto; &__header--one-item, &__header--multiple-items { @@ -33,15 +34,16 @@ } &__wrapper { - width: 92%; display: flex; + overflow: hidden; + width: 100%; } &__list { flex: 2 1 0; - width: 92%; + width: 100%; max-height: max-content; - border: 1px solid #d0d5da; + border: 1px solid var(--color-border-muted); box-sizing: border-box; border-radius: 8px; margin-top: 8px; @@ -52,7 +54,7 @@ display: flex; align-items: center; padding: 16px; - border-bottom: 1px solid #d2d8dd; + border-bottom: 1px solid var(--color-border-muted); justify-content: space-between; &:last-of-type { @@ -60,7 +62,7 @@ } &:hover { - background: var(--Grey-000); + background: var(--color-background-alternative); cursor: pointer; } @@ -74,7 +76,7 @@ &__label { @include H6; - color: var(--Black-100); + color: var(--color-text-default); text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -83,7 +85,7 @@ &__balance { @include H7; - color: var(--Grey-500); + color: var(--color-text-default); } &__last-connected { @@ -92,7 +94,7 @@ display: flex; flex-direction: column; align-items: flex-end; - color: var(--primary-blue); + color: var(--color-primary-default); } } @@ -112,14 +114,14 @@ } .fa-info-circle { - color: var(--Grey-200); + color: var(--color-icon-muted); cursor: pointer; margin-inline-start: 8px; font-size: 0.9rem; } .fa-info-circle:hover { - color: var(--Grey-300); + color: var(--color-icon-default); } &__text, @@ -129,11 +131,11 @@ } &__text-blue { - color: var(--primary-blue); + color: var(--color-primary-default); cursor: pointer; } &__text-grey { - color: var(--Grey-500); + color: var(--color-text-default); } } diff --git a/ui/components/ui/actionable-message/actionable-message.js b/ui/components/ui/actionable-message/actionable-message.js index 70a239704..bfad8da6b 100644 --- a/ui/components/ui/actionable-message/actionable-message.js +++ b/ui/components/ui/actionable-message/actionable-message.js @@ -6,13 +6,13 @@ import InfoTooltipIcon from '../info-tooltip/info-tooltip-icon'; const CLASSNAME_WARNING = 'actionable-message--warning'; const CLASSNAME_DANGER = 'actionable-message--danger'; -const CLASSNAME_INFO = 'actionable-message--info'; +const CLASSNAME_SUCCESS = 'actionable-message--success'; const CLASSNAME_WITH_RIGHT_BUTTON = 'actionable-message--with-right-button'; -const typeHash = { +export const typeHash = { warning: CLASSNAME_WARNING, danger: CLASSNAME_DANGER, - info: CLASSNAME_INFO, + success: CLASSNAME_SUCCESS, default: '', }; @@ -136,7 +136,7 @@ ActionableMessage.propTypes = { /** * change color theme for the component that already predefined in css */ - type: PropTypes.string, + type: PropTypes.oneOf(Object.keys(typeHash)), /** * change text align to left and button to bottom right */ diff --git a/ui/components/ui/actionable-message/actionable-message.stories.js b/ui/components/ui/actionable-message/actionable-message.stories.js index afeb1eafb..6ce95259a 100644 --- a/ui/components/ui/actionable-message/actionable-message.stories.js +++ b/ui/components/ui/actionable-message/actionable-message.stories.js @@ -1,5 +1,14 @@ import React from 'react'; + +import Box from '../box'; +import Typography from '../typography'; +import { + COLORS, + DISPLAY, + FLEX_WRAP, +} from '../../../helpers/constants/design-system'; import README from './README.mdx'; +import { typeHash } from './actionable-message'; import ActionableMessage from '.'; export default { @@ -21,6 +30,11 @@ export default { infoTooltipText: { control: 'text' }, useIcon: { control: 'boolean' }, iconFillColor: { control: 'color' }, + roundedButtons: { control: 'boolean' }, + }, + args: { + message: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', }, }; @@ -45,16 +59,26 @@ export const DefaultStory = (args) => ( DefaultStory.storyName = 'Default'; -DefaultStory.args = { - message: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +export const Type = (args) => ( + <> + {Object.keys(typeHash).map((type) => ( + + ))} + +); + +Type.args = { + message: '', }; export const OneAction = (args) => ; OneAction.args = { - message: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', primaryAction: { label: 'Dismiss', }, @@ -63,8 +87,6 @@ OneAction.args = { export const TwoActions = (args) => ; TwoActions.args = { - message: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', primaryAction: { label: 'Dismiss', }, @@ -77,8 +99,6 @@ TwoActions.args = { export const LeftAligned = (args) => ; LeftAligned.args = { - message: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', primaryAction: { label: 'Dismiss', }, @@ -88,8 +108,6 @@ LeftAligned.args = { export const WithIcon = (args) => ; WithIcon.args = { - message: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', className: 'actionable-message--left-aligned actionable-message--warning', useIcon: true, iconFillColor: '#f8c000', @@ -107,3 +125,30 @@ PrimaryV2Action.args = { label: 'I want to proceed anyway', }, }; + +export const OnTopOfContent = (args) => { + return ( +
    + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + + +
    + +
    +
    + ); +}; diff --git a/ui/components/ui/actionable-message/index.scss b/ui/components/ui/actionable-message/index.scss index 1dd770839..0bfdda370 100644 --- a/ui/components/ui/actionable-message/index.scss +++ b/ui/components/ui/actionable-message/index.scss @@ -1,6 +1,7 @@ .actionable-message { - background: var(--Blue-000); - border: 1px solid var(--Blue-200); + color: var(--color-text-default); + background-color: var(--color-background-default); + border: 1px solid var(--color-info-default); border-radius: 8px; padding: 16px; margin-top: 18px; @@ -9,6 +10,28 @@ align-items: center; position: relative; + /** + * Need the z-index and pseudo element for the time being while we update our muted colors to not use alpha levels + * Otherwise ActionableMessages on top of content will transparent and it may effect + * readability. + */ + + * { + z-index: 1; + } + + &::before { + content: ''; + position: absolute; + border-radius: 8px; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: var(--color-info-muted); + } + + &--with-icon { padding-inline-start: 32px; } @@ -31,7 +54,7 @@ @include H7; &__message { - color: var(--Blue-600); + color: var(--color-text-default); text-align: center; width: 100%; } @@ -42,7 +65,7 @@ justify-content: flex-end; align-items: center; margin-top: 10px; - color: var(--Blue-600); + color: var(--color-primary-default); &--single { width: 100%; @@ -55,15 +78,10 @@ &--rounded { border-radius: 8px; } - - &-danger { - background: var(--Red-500); - color: #fff; - } } &__action-v2 { - color: var(--primary-1); + color: var(--color-primary-default); background: none; border: none; font-size: 12px; @@ -79,13 +97,11 @@ } &--warning { - background: var(--Yellow-100); - border: 1px solid var(--Yellow-500); + border: 1px solid var(--color-warning-default); justify-content: center; - .actionable-message__message, - .actionable-message__action { - color: var(--Black-100); + &::before { + background: var(--color-warning-muted); } .actionable-message__action--secondary { @@ -94,22 +110,23 @@ } &--danger { - background: var(--Red-000); - border: 1px solid var(--Red-300); + border: 1px solid var(--color-error-default); justify-content: flex-start; + &::before { + background: var(--color-error-muted); + } + .actionable-message__message { - color: var(--Black-100); text-align: left; } } - &--info { - background: var(--Green-000); - border: 1px solid var(--Green-200); + &--success { + border: 1px solid var(--color-success-default); - .actionable-message__message { - color: var(--Black-100); + &::before { + background: var(--color-success-muted); } } @@ -150,6 +167,8 @@ .actionable-message--warning.actionable-message--with-right-button { .actionable-message__action { - background: var(--Yellow-500); + &::before { + background: var(--color-warning-muted); + } } } 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) => ( +
    +
    +
    +
    +
    +
    + +
    +
    diff --git a/ui/components/ui/button/buttons.scss b/ui/components/ui/button/buttons.scss index c0f61531f..5bbe4cd96 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-primary-inverse); + 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/definition-list/definition-list.js b/ui/components/ui/definition-list/definition-list.js index 50a11b7a1..05ca7b912 100644 --- a/ui/components/ui/definition-list/definition-list.js +++ b/ui/components/ui/definition-list/definition-list.js @@ -53,7 +53,7 @@ export default function DefinitionList({ details[open] > summary { - &::before { - background-image: url("images/icons/collapse.svg"); - } - } - &__summary { position: relative; padding-left: 24px; @@ -17,16 +11,16 @@ &::-webkit-details-marker, &::marker { display: none; - content: ""; + content: ''; } &::before { position: absolute; - content: " "; + content: ' '; flex: 0 0 auto; height: 16px; width: 16px; - background-image: url("images/icons/expand.svg"); + background-image: url('images/icons/expand.svg'); background-size: contain; background-repeat: no-repeat; cursor: pointer; diff --git a/ui/components/ui/disclosure/disclosure.stories.js b/ui/components/ui/disclosure/disclosure.stories.js new file mode 100644 index 000000000..af9e8a097 --- /dev/null +++ b/ui/components/ui/disclosure/disclosure.stories.js @@ -0,0 +1,27 @@ +import React from 'react'; +import Disclosure from '.'; + +export default { + title: 'Components/UI/Disclosure', // title should follow the folder structure location of the component. Don't use spaces. + id: __filename, + argTypes: { + children: { + control: 'text', + }, + title: { + control: 'text', + }, + size: { + control: 'text', + }, + }, + args: { + title: 'title', + children: 'hello world', + size: 'normal', + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/ui/dropdown/dropdown.js b/ui/components/ui/dropdown/dropdown.js index 5b678a5fb..3a6de0dca 100644 --- a/ui/components/ui/dropdown/dropdown.js +++ b/ui/components/ui/dropdown/dropdown.js @@ -1,6 +1,7 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import IconCaretDown from '../icon/icon-caret-down'; const Dropdown = ({ className, @@ -21,22 +22,25 @@ const Dropdown = ({ ); return ( - +
    + + +
    ); }; diff --git a/ui/components/ui/dropdown/dropdown.scss b/ui/components/ui/dropdown/dropdown.scss index ffcf9c99a..7311bbac0 100644 --- a/ui/components/ui/dropdown/dropdown.scss +++ b/ui/components/ui/dropdown/dropdown.scss @@ -1,21 +1,36 @@ .dropdown { - @include H6; + position: relative; + display: inline-block; - appearance: none; + &__select { + appearance: none; + // TODO: remove these after getting autoprefixer working in Storybook + -moz-appearance: none; + -webkit-appearance: none; - // TODO: remove these after getting autoprefixer working in Storybook - -moz-appearance: none; - -webkit-appearance: none; - border: 1px solid #d2d8dd; - border-radius: 6px; - background-image: url('/images/icons/caret-down.svg'); - background-repeat: no-repeat, repeat; - background-position: right 18px top 50%; - background-color: white; - padding: 8px 32px 8px 16px; + @include H6; - [dir='rtl'] & { - background-position: left 18px top 50%; - padding: 8px 16px 8px 32px; + color: var(--color-text-default); + border: 1px solid var(--color-border-default); + border-radius: 4px; + background-color: var(--color-background-default); + padding: 8px 40px 8px 16px; + width: 100%; + + [dir='rtl'] & { + padding: 8px 16px 8px 40px; + } + } + + &__icon-caret-down { + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + + [dir='rtl'] & { + left: 16px; + } } } diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js index c97353ff9..08e1a5c0b 100644 --- a/ui/components/ui/form-field/form-field.js +++ b/ui/components/ui/form-field/form-field.js @@ -32,6 +32,8 @@ export default function FormField({ disabled, placeholder, warning, + passwordStrength, + passwordStrengthText, }) { return (
    {titleUnit} @@ -106,7 +108,7 @@ export default function FormField({ )} {error && ( @@ -115,13 +117,31 @@ export default function FormField({ )} {warning && ( {warning} )} + {passwordStrength && ( + + {passwordStrength} + + )} + {passwordStrengthText && ( + + {passwordStrengthText} + + )}
    ); @@ -192,6 +212,14 @@ FormField.propTypes = { * Set the placeholder text for the input field */ placeholder: PropTypes.string, + /** + * Show password strength according to the score + */ + passwordStrength: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** + * Show password strength description + */ + passwordStrengthText: PropTypes.string, }; FormField.defaultProps = { diff --git a/ui/components/ui/form-field/index.scss b/ui/components/ui/form-field/index.scss index 6182f9af9..c281b9034 100644 --- a/ui/components/ui/form-field/index.scss +++ b/ui/components/ui/form-field/index.scss @@ -18,36 +18,39 @@ &__error, &__error h6 { - color: var(--error-1) !important; + color: var(--color-error-default) !important; padding-top: 6px; } h6 { padding-bottom: 6px; margin-inline-end: 6px; + color: var(--color-text-default); } i { - color: #dadada; + color: var(--color-icon-default); font-size: $font-size-h7; } &__input { width: 100%; - border: solid 1px var(--ui-3); padding: 10px; border-radius: 6px; + background-color: var(--color-background-default); + color: var(--color-text-default); + border: 2px solid var(--color-border-default); &:focus { - border: solid 2px var(--primary-1); + border: solid 2px var(--color-primary-default); } &--error { - border-color: var(--error-1); + border-color: var(--color-error-default); } &--warning { - border-color: var(--alert-3); + border-color: var(--color-warning-default); } } } diff --git a/ui/components/ui/icon/icon-caret-down.js b/ui/components/ui/icon/icon-caret-down.js new file mode 100644 index 000000000..64b40a31a --- /dev/null +++ b/ui/components/ui/icon/icon-caret-down.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const IconCaretDown = ({ + size = 24, + color = 'currentColor', + ariaLabel, + className, +}) => ( + + + +); + +IconCaretDown.propTypes = { + /** + * The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc + */ + size: PropTypes.number, + /** + * The color of the icon accepts design token css variables + */ + color: PropTypes.string, + /** + * An additional className to assign the Icon + */ + className: PropTypes.string, + /** + * The aria-label of the icon for accessibility purposes + */ + ariaLabel: PropTypes.string, +}; + +export default IconCaretDown; diff --git a/ui/components/ui/icon/icon-caret-left.js b/ui/components/ui/icon/icon-caret-left.js new file mode 100644 index 000000000..4fe22144e --- /dev/null +++ b/ui/components/ui/icon/icon-caret-left.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const IconCaretLeft = ({ + size = 24, + color = 'currentColor', + ariaLabel, + className, + onClick, +}) => ( + + + +); + +IconCaretLeft.propTypes = { + /** + * The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc + */ + size: PropTypes.number, + /** + * The color of the icon accepts design token css variables + */ + color: PropTypes.string, + /** + * An additional className to assign the Icon + */ + className: PropTypes.string, + /** + * The onClick handler + */ + onClick: PropTypes.func, + /** + * The aria-label of the icon for accessibility purposes + */ + ariaLabel: PropTypes.string, +}; + +export default IconCaretLeft; diff --git a/ui/components/ui/icon/icon-caret-right.js b/ui/components/ui/icon/icon-caret-right.js new file mode 100644 index 000000000..890f89171 --- /dev/null +++ b/ui/components/ui/icon/icon-caret-right.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const IconCaretRight = ({ + size = 24, + color = 'currentColor', + ariaLabel, + className, +}) => ( + + + +); + +IconCaretRight.propTypes = { + /** + * The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc + */ + size: PropTypes.number, + /** + * The color of the icon accepts design token css variables + */ + color: PropTypes.string, + /** + * An additional className to assign the Icon + */ + className: PropTypes.string, + /** + * The aria-label of the icon for accessibility purposes + */ + ariaLabel: PropTypes.string, +}; + +export default IconCaretRight; diff --git a/ui/components/ui/icon/icon-caret-up.js b/ui/components/ui/icon/icon-caret-up.js new file mode 100644 index 000000000..8e455e638 --- /dev/null +++ b/ui/components/ui/icon/icon-caret-up.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const IconCaretUp = ({ + size = 24, + color = 'currentColor', + ariaLabel, + className, +}) => ( + + + +); + +IconCaretUp.propTypes = { + /** + * The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc + */ + size: PropTypes.number, + /** + * The color of the icon accepts design token css variables + */ + color: PropTypes.string, + /** + * An additional className to assign the Icon + */ + className: PropTypes.string, + /** + * The aria-label of the icon for accessibility purposes + */ + ariaLabel: PropTypes.string, +}; + +export default IconCaretUp; diff --git a/ui/components/ui/icon/icon.stories.js b/ui/components/ui/icon/icon.stories.js index 74c6e78d6..030841470 100644 --- a/ui/components/ui/icon/icon.stories.js +++ b/ui/components/ui/icon/icon.stories.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { SEVERITIES } from '../../../helpers/constants/design-system'; +import { COLORS, SEVERITIES } from '../../../helpers/constants/design-system'; import Card from '../card'; import Typography from '../typography'; import Box from '../box'; @@ -22,6 +22,10 @@ import SunCheck from './sun-check-icon.component'; import Swap from './swap-icon-for-list.component'; import SwapIcon from './overview-send-icon.component'; import SwapIconComponent from './swap-icon.component'; +import IconCaretLeft from './icon-caret-left'; +import IconCaretRight from './icon-caret-right'; +import IconCaretDown from './icon-caret-down'; +import IconCaretUp from './icon-caret-up'; import IconEye from './icon-eye'; import IconEyeSlash from './icon-eye-slash'; @@ -37,7 +41,12 @@ export default { const IconItem = ({ Component }) => { return ( - + {Component} {`${Component.type.__docgenInfo.displayName}`} @@ -106,6 +115,10 @@ export const DefaultStory = (args) => ( } /> } /> } /> + } /> + } /> + } /> + } /> } /> } />
    @@ -116,9 +129,9 @@ export const DefaultStory = (args) => ( DefaultStory.args = { width: '17', height: '21', - fill: '#2F80ED', + fill: 'var(--color-icon-default)', size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', severity: 'info', reverseColors: false, }; @@ -126,42 +139,42 @@ DefaultStory.args = { export const ApproveStory = (args) => ; ApproveStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; ApproveStory.storyName = 'Approve'; export const SignStory = (args) => ; SignStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; SignStory.storyName = 'Sign'; export const SwapStory = (args) => ; SwapStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; SwapStory.storyName = 'Swap'; export const SendIconStory = (args) => ; SendIconStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; SendIconStory.storyName = 'SendIcon'; export const ReceiveIconStory = (args) => ; ReceiveIconStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; ReceiveIconStory.storyName = 'ReceiveIcon'; export const InteractionStory = (args) => ; InteractionStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; InteractionStory.storyName = 'Interaction'; @@ -220,21 +233,21 @@ export const SendSwapIconStory = (args) => ; SendSwapIconStory.args = { width: '17', height: '17', - color: '#2F80ED', + color: 'var(--color-icon-default)', }; SendSwapIconStory.storyName = 'Send/SwapIcon'; export const PaperAirplaneStory = (args) => ; PaperAirplaneStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; PaperAirplaneStory.storyName = 'PaperAirplane'; export const CopyIconStory = (args) => ; CopyIconStory.args = { size: 40, - color: '#2F80ED', + color: 'var(--color-icon-default)', }; CopyIconStory.storyName = 'CopyIcon'; diff --git a/ui/components/ui/icon/index.scss b/ui/components/ui/icon/index.scss index d05d6a433..c0a0e009f 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-info-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/metafox-logo/horizontal-logo.js b/ui/components/ui/metafox-logo/horizontal-logo.js new file mode 100644 index 000000000..920f5d274 --- /dev/null +++ b/ui/components/ui/metafox-logo/horizontal-logo.js @@ -0,0 +1,965 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const LOGO_WIDTH = 162; +const LOGO_HEIGHT = 30; +const TEXT_COLOR = 'var(--color-text-default)'; +const FLASK_PILL_BACKGROUND = 'var(--color-overlay-alternative)'; +const FLASK_PILL_TEXT = 'var(--color-overlay-inverse)'; +const BETA_PILL_BACKGROUND = 'var(--color-secondary-default)'; +const BETA_PIL_TEXT = 'var(--color-secondary-inverse)'; + +export default function MetaFoxHorizontalLogo({ className }) { + switch (process.env.METAMASK_BUILD_TYPE) { + case 'beta': + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + case 'flask': + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + default: + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} + +MetaFoxHorizontalLogo.propTypes = { + className: PropTypes.string, +}; diff --git a/ui/components/ui/metafox-logo/metafox-logo.component.js b/ui/components/ui/metafox-logo/metafox-logo.component.js index 04a626fd0..7dd6c9fcc 100644 --- a/ui/components/ui/metafox-logo/metafox-logo.component.js +++ b/ui/components/ui/metafox-logo/metafox-logo.component.js @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import MetaFoxHorizontalLogo from './horizontal-logo'; export default class MetaFoxLogo extends PureComponent { static propTypes = { @@ -26,14 +27,11 @@ export default class MetaFoxLogo extends PureComponent { 'app-header__logo-container--clickable': Boolean(onClick), })} > - {detailText && ( - + {detailText} )} diff --git a/ui/components/ui/numeric-input/numeric-input.scss b/ui/components/ui/numeric-input/numeric-input.scss index 897e65de0..f90d3de87 100644 --- a/ui/components/ui/numeric-input/numeric-input.scss +++ b/ui/components/ui/numeric-input/numeric-input.scss @@ -1,5 +1,5 @@ .numeric-input { - border: 1px solid var(--ui-3); + border: 1px solid var(--color-border-default); position: relative; border-radius: 6px; @@ -12,6 +12,8 @@ border: 0; padding: 10px; border-radius: 6px; + background-color: var(--color-background-default); + color: var(--color-text-default); /* ensures the increment/decrement arrows always display */ &::-webkit-inner-spin-button, diff --git a/ui/components/ui/radio-group/radio-group.component.js b/ui/components/ui/radio-group/radio-group.component.js index a0501b4fc..0704de2a2 100644 --- a/ui/components/ui/radio-group/radio-group.component.js +++ b/ui/components/ui/radio-group/radio-group.component.js @@ -48,7 +48,7 @@ export default function RadioGroup({ options, name, selectedValue, onChange }) {
    ) : (
    - +
    ); } diff --git a/ui/components/ui/sender-to-recipient/sender-to-recipient.stories.js b/ui/components/ui/sender-to-recipient/sender-to-recipient.stories.js new file mode 100644 index 000000000..8779e9b20 --- /dev/null +++ b/ui/components/ui/sender-to-recipient/sender-to-recipient.stories.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { + DEFAULT_VARIANT, + CARDS_VARIANT, + FLAT_VARIANT, +} from './sender-to-recipient.constants'; +import SenderToRecipient from '.'; + +export default { + title: 'Components/UI/SenderToRecipient', + id: __filename, + argTypes: { + senderName: { + control: 'text', + }, + senderAddress: { + control: 'text', + }, + recipientName: { + control: 'text', + }, + recipientEns: { + control: 'text', + }, + recipientAddress: { + control: 'text', + }, + recipientNickname: { + control: 'text', + }, + variant: { + control: 'select', + options: [DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT], + }, + addressOnly: { + control: 'boolean', + }, + onRecipientClick: { + action: 'onRecipientClick', + }, + onSenderClick: { + action: 'onSenderClick', + }, + warnUserOnAccountMismatch: { + control: 'boolean', + }, + }, + args: { + senderName: 'Account 1', + senderAddress: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + recipientName: 'Account 2', + recipientAddress: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/ui/site-origin/index.scss b/ui/components/ui/site-origin/index.scss index 16b9f0732..2f0016e76 100644 --- a/ui/components/ui/site-origin/index.scss +++ b/ui/components/ui/site-origin/index.scss @@ -1,4 +1,10 @@ .site-origin { + max-width: 100%; + + .chip { + max-width: 100%; + } + .chip__left-icon { padding: 4px 0 4px 8px; } diff --git a/ui/components/ui/slider/slider.component.js b/ui/components/ui/slider/slider.component.js index 63be28236..69c05ba26 100644 --- a/ui/components/ui/slider/slider.component.js +++ b/ui/components/ui/slider/slider.component.js @@ -80,14 +80,20 @@ const Slider = ({ )} {valueText && ( - + {valueText} )} {titleDetail && (
    - + {titleDetail}
    @@ -97,7 +103,10 @@ const Slider = ({
    {infoText && ( - + {infoText} )} diff --git a/ui/components/ui/text-field/index.scss b/ui/components/ui/text-field/index.scss new file mode 100644 index 000000000..a33cf5630 --- /dev/null +++ b/ui/components/ui/text-field/index.scss @@ -0,0 +1,4 @@ +.MuiInput-input, +.MuiInputBase-input { + color: var(--color-text-default) !important; +} diff --git a/ui/components/ui/text-field/text-field.component.js b/ui/components/ui/text-field/text-field.component.js index e83df1717..f4062cf4c 100644 --- a/ui/components/ui/text-field/text-field.component.js +++ b/ui/components/ui/text-field/text-field.component.js @@ -7,39 +7,42 @@ const inputLabelBase = { transform: 'none', transition: 'none', position: 'initial', - color: '#5b5b5b', + color: 'var(--color-text-default)', }; const styles = { materialLabel: { '&$materialFocused': { - color: '#aeaeae', + color: 'var(--color-text-alternative)', }, '&$materialError': { - color: '#aeaeae', + color: 'var(--color-text-alternative)', }, fontWeight: '400', - color: '#aeaeae', + color: 'var(--color-text-alternative)', }, materialFocused: {}, materialUnderline: { + '&:before': { + borderBottom: '1px solid var(--color-text-default) !important', // Visible bottom border + }, '&:after': { - borderBottom: `2px solid rgb(3, 125, 214)`, + borderBottom: `2px solid rgb(3, 125, 214)`, // Animated bottom border }, }, materialError: {}, materialWhitePaddedRoot: { - color: '#aeaeae', + color: 'var(--color-text-alternative)', }, materialWhitePaddedInput: { padding: '8px', '&::placeholder': { - color: '#aeaeae', + color: 'var(--color-text-alternative)', }, }, materialWhitePaddedFocused: { - color: '#fff', + color: 'var(--color-background-default)', }, materialWhitePaddedUnderline: { '&:after': { @@ -61,14 +64,15 @@ const styles = { 'label + &': { marginTop: '9px', }, - border: '1px solid #BBC0C5', + border: '1px solid var(--color-border-default)', + color: 'var(--color-text-default)', height: '48px', borderRadius: '6px', padding: '0 16px', display: 'flex', alignItems: 'center', '&$inputFocused': { - border: '1px solid #2f9ae0', + border: '1px solid var(--color-primary-default)', }, }, largeInputLabel: { @@ -88,6 +92,7 @@ const getMaterialThemeInputProps = ({ dir, classes: { materialLabel, materialFocused, materialError, materialUnderline }, startAdornment, + endAdornment, min, max, autoComplete, @@ -101,6 +106,7 @@ const getMaterialThemeInputProps = ({ }, InputProps: { startAdornment, + endAdornment, classes: { underline: materialUnderline, }, @@ -122,12 +128,14 @@ const getMaterialWhitePaddedThemeInputProps = ({ materialWhitePaddedUnderline, }, startAdornment, + endAdornment, min, max, autoComplete, }) => ({ InputProps: { startAdornment, + endAdornment, classes: { root: materialWhitePaddedRoot, focused: materialWhitePaddedFocused, @@ -157,6 +165,7 @@ const getBorderedThemeInputProps = ({ }, largeLabel, startAdornment, + endAdornment, min, max, autoComplete, @@ -172,6 +181,7 @@ const getBorderedThemeInputProps = ({ }, InputProps: { startAdornment, + endAdornment, disableUnderline: true, classes: { root: inputRoot, @@ -199,6 +209,7 @@ const TextField = ({ classes, theme, startAdornment, + endAdornment, largeLabel, dir, min, @@ -210,6 +221,7 @@ const TextField = ({ const inputProps = themeToInputProps[theme]({ classes, startAdornment, + endAdornment, largeLabel, dir, min, @@ -263,6 +275,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 ( - - - - ## Component API + +## Usage + +The following describes the props and example usage for this component. + +### Variant + +Use the `variant` prop and the `TYPOGRAPHY` object from `./ui/helpers/constants/design-system.js` to change the font size of the Typography component. + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { TYPOGRAPHY} from '../../../helpers/constants/design-system'; + +h1 +h2 +h3 +h4 +h5 +h6 +h7 +h8 +h9 +p +``` + +### Color + +Use the `color` prop and the `COLOR` object from `./ui/helpers/constants/design-system.js` to change the color of the Typography component. + + + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { COLORS} from '../../../helpers/constants/design-system'; + + + text-default + + + text-alternative + + + text-muted + + + overlay-inverse + + + primary-default + + + primary-inverse + + + secondary-default + + + secondary-inverse + + + error-default + + + error-inverse + + + success-default + + + success-inverse + + + warning-inverse + + + info-default + + + info-inverse + +``` + +## Deprecated Colors + +List of deprecated 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', +``` + +### Font Weight + +Use the `fontWeight` prop and the `FONT_WEIGHT` object from `./ui/helpers/constants/design-system.js` to change the font weight of the Typography component. There are 2 font weights: + +- `FONT_WEIGHT.NORMAL` = `normal` || `400` +- `FONT_WEIGHT.BOLD` = `bold` || `700` + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { FONT_WEIGHT } from '../../../helpers/constants/design-system'; + + + normal + + + bold + +``` + +### Font Style + +Use the `fontStyle` prop and the `FONT_STYLE` object from `./ui/helpers/constants/design-system.js` to change the font style of the Typography component. There are 2 font styles: + +- `FONT_STYLE.NORMAL` +- `FONT_STYLE.ITALIC` + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { FONT_STYLE } from '../../../helpers/constants/design-system'; + + + normal + + + bold + +``` + +### Align + +Use the `align` prop and the `TEXT_ALIGN` object from `./ui/helpers/constants/design-system.js` to change the text alignment of the Typography component. + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { TEXT_ALIGN } from '../../../helpers/constants/design-system'; + + + left + + + center + + + right + + + justify + + + end + +``` + +### Overflow Wrap + +Use the `overflowWrap` prop and the `OVERFLOW_WRAP` object from `./ui/helpers/constants/design-system.js` to change the overflow wrap of the Typography component. + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { OVERFLOW_WRAP } from '../../../helpers/constants/design-system'; + +
    + + {OVERFLOW_WRAP.NORMAL}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d + + + {OVERFLOW_WRAP.BREAK_WORD}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d + +
    ; +``` + +### Tag + +Use the `tag` prop to change the root html element of the Typography component + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; + +dd +div +dt +em +h1 +h2 +h3 +h4 +h5 +h6 +li +p +span +strong +``` + +Renders the html: + +```html +
    dd
    +
    div
    +
    dt
    +em +

    h1

    +

    h2

    +

    h3

    +

    h4

    +
    h5
    +
    h6
    +
  • li
  • +

    p

    +span +strong +``` + +### Margin + +Use the `margin` prop to change margin of the Typography component. For more control over bounding box properties use the `boxProps` props object. + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; + +This uses the boxProps prop; +``` + +### Box Props + +Use the `boxProps` prop object to pass any valid [Box](/?path=/story/ui-components-ui-box-box-stories-js--default-story) component props to the Typography component. `boxProps` will overwrite the `margin` prop + + + + + +```jsx +// If importing from ui/components/app/[YOUR_COMPONENT]/ directory +import Typography from '../../ui/typography'; +import { COLORS } from '../../../helpers/constants/design-system'; + + + This uses the boxProps prop +; +``` + +### Class Name + +Adds an additional class to the Typography component + +### Children + +The text content of the typography component diff --git a/ui/components/ui/typography/typography.js b/ui/components/ui/typography/typography.js index 8c9e4e272..166ad4ac1 100644 --- a/ui/components/ui/typography/typography.js +++ b/ui/components/ui/typography/typography.js @@ -13,24 +13,94 @@ import Box, { MultipleSizes } from '../box'; const { H6, H7, H8, H9 } = TYPOGRAPHY; +export const ValidColors = [ + COLORS.TEXT_DEFAULT, + COLORS.TEXT_ALTERNATIVE, + COLORS.TEXT_MUTED, + COLORS.OVERLAY_INVERSE, + COLORS.PRIMARY_DEFAULT, + COLORS.PRIMARY_INVERSE, + COLORS.SECONDARY_DEFAULT, + COLORS.SECONDARY_INVERSE, + COLORS.ERROR_DEFAULT, + COLORS.ERROR_INVERSE, + COLORS.SUCCESS_DEFAULT, + COLORS.SUCCESS_INVERSE, + COLORS.WARNING_INVERSE, + COLORS.INFO_DEFAULT, + COLORS.INFO_INVERSE, + /** + * COLORS BELOW HAVE BEEN DEPRECATED + */ + COLORS.UI1, + COLORS.UI2, + COLORS.UI3, + COLORS.UI4, + COLORS.BLACK, + COLORS.GREY, + COLORS.NEUTRAL_GREY, + COLORS.WHITE, + COLORS.PRIMARY1, + COLORS.PRIMARY2, + COLORS.PRIMARY3, + COLORS.SECONDARY1, + COLORS.SECONDARY2, + COLORS.SECONDARY3, + COLORS.SUCCESS1, + COLORS.SUCCESS2, + COLORS.SUCCESS3, + COLORS.ERROR1, + COLORS.ERROR2, + COLORS.ERROR3, + COLORS.ALERT1, + COLORS.ALERT2, + COLORS.ALERT3, +]; + +export const ValidTags = [ + 'dd', + 'div', + 'dt', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'p', + 'span', + 'strong', + 'ul', +]; + export default function Typography({ variant = TYPOGRAPHY.Paragraph, - className, - color = COLORS.BLACK, - tag, - children, + color = COLORS.TEXT_DEFAULT, fontWeight = 'normal', fontStyle = 'normal', align, overflowWrap, - boxProps = {}, + title, + tag, margin = [1, 0], + boxProps = {}, + className, + children, }) { + let Tag = tag ?? variant; + let strongTagFontWeight; + + if (Tag === 'strong') { + strongTagFontWeight = FONT_WEIGHT.BOLD; + } + const computedClassName = classnames( 'typography', className, `typography--${variant}`, - `typography--weight-${fontWeight}`, + `typography--weight-${strongTagFontWeight || fontWeight}`, `typography--style-${fontStyle}`, { [`typography--align-${align}`]: Boolean(align), @@ -39,8 +109,6 @@ export default function Typography({ }, ); - let Tag = tag ?? variant; - if (Tag === TYPOGRAPHY.Paragraph) { Tag = 'p'; } else if ([H7, H8, H9].includes(Tag)) { @@ -50,7 +118,10 @@ export default function Typography({ return ( {(boxClassName) => ( - + {children} )} @@ -59,33 +130,61 @@ export default function Typography({ } Typography.propTypes = { + /** + * The variation of font sizes of the Typography component + */ variant: PropTypes.oneOf(Object.values(TYPOGRAPHY)), - children: PropTypes.node.isRequired, - color: PropTypes.oneOf(Object.values(COLORS)), - className: PropTypes.string, + /** + * The color of the Typography component Should use the COLOR object from + * ./ui/helpers/constants/design-system.js + */ + color: PropTypes.oneOf(ValidColors), + /** + * The font-weight of the Typography component. Should use the FONT_WEIGHT object from + * ./ui/helpers/constants/design-system.js + */ + fontWeight: PropTypes.oneOf(Object.values(FONT_WEIGHT)), + /** + * The font-style of the Typography component. Should use the FONT_STYLE object from + * ./ui/helpers/constants/design-system.js + */ + fontStyle: PropTypes.oneOf(Object.values(FONT_STYLE)), + /** + * The text-align of the Typography component. Should use the TEXT_ALIGN object from + * ./ui/helpers/constants/design-system.js + */ align: PropTypes.oneOf(Object.values(TEXT_ALIGN)), + /** + * The overflow-wrap of the Typography component. Should use the OVERFLOW_WRAP object from + * ./ui/helpers/constants/design-system.js + */ + overflowWrap: PropTypes.oneOf(Object.values(OVERFLOW_WRAP)), + /** + * Changes the root html element tag of the Typography component. + */ + tag: PropTypes.oneOf(ValidTags), + /** + * Adds margin to the Typography component should use valid sizes + * 1,2,4,6,8 or an array of those values + */ + margin: MultipleSizes, + /** + * Used to pass any valid Box component props such as margin or padding + * to the Typography component + */ boxProps: PropTypes.shape({ ...Box.propTypes, }), - margin: MultipleSizes, - fontWeight: PropTypes.oneOf(Object.values(FONT_WEIGHT)), - fontStyle: PropTypes.oneOf(Object.values(FONT_STYLE)), - overflowWrap: PropTypes.oneOf(Object.values(OVERFLOW_WRAP)), - tag: PropTypes.oneOf([ - 'p', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'span', - 'strong', - 'em', - 'li', - 'div', - 'dt', - 'dd', - 'ul', - ]), + /** + * Additional className to assign the Typography component + */ + className: PropTypes.string, + /** + * Title attribute to include on the element. Will show as tooltip on hover. + */ + title: PropTypes.string, + /** + * The text content of the Typography component + */ + children: PropTypes.node.isRequired, }; diff --git a/ui/components/ui/typography/typography.stories.js b/ui/components/ui/typography/typography.stories.js index 119860c25..d36024ba4 100644 --- a/ui/components/ui/typography/typography.stories.js +++ b/ui/components/ui/typography/typography.stories.js @@ -2,12 +2,20 @@ import React from 'react'; import { COLORS, FONT_WEIGHT, + FONT_STYLE, TEXT_ALIGN, TYPOGRAPHY, + OVERFLOW_WRAP, } from '../../../helpers/constants/design-system'; + +import { ValidColors, ValidTags } from './typography'; + import README from './README.mdx'; import Typography from '.'; +const sizeKnobOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; +const marginSizeKnobOptions = [...sizeKnobOptions, 'auto']; + export default { title: 'Components/UI/Typography', id: __filename, @@ -17,49 +25,265 @@ export default { }, }, argTypes: { + variant: { + control: { type: 'select' }, + options: Object.values(TYPOGRAPHY), + }, color: { control: { type: 'select' }, - options: COLORS, - defaultValue: COLORS.BLACK, - }, - align: { - control: { type: 'select' }, - options: TEXT_ALIGN, - defaultValue: TEXT_ALIGN.LEFT, + options: ValidColors, }, fontWeight: { control: { type: 'select' }, - options: FONT_WEIGHT, - defaultValue: FONT_WEIGHT.NORMAL, + options: Object.values(FONT_WEIGHT), }, - variant: { + fontStyle: { control: { type: 'select' }, - options: TYPOGRAPHY, - defaultValue: TYPOGRAPHY.Paragraph, + options: Object.values(FONT_STYLE), }, - content: { + align: { + control: { type: 'select' }, + options: Object.values(TEXT_ALIGN), + }, + overflowWrap: { + control: { type: 'select' }, + options: Object.values(OVERFLOW_WRAP), + }, + tag: { + control: { type: 'select' }, + options: ValidTags, + }, + margin: { + options: marginSizeKnobOptions, + control: 'select', + }, + boxProps: { + control: 'object', + }, + className: { + control: { type: 'text' }, + }, + children: { control: { type: 'text' }, - defaultValue: 'The quick orange fox jumped over the lazy dog.', }, }, }; +function renderBackgroundColor(color) { + let bgColor; + switch (color) { + case COLORS.OVERLAY_INVERSE: + bgColor = COLORS.OVERLAY_DEFAULT; + break; + case COLORS.PRIMARY_INVERSE: + bgColor = COLORS.PRIMARY_DEFAULT; + break; + case COLORS.SECONDARY_INVERSE: + bgColor = COLORS.SECONDARY_DEFAULT; + break; + case COLORS.ERROR_INVERSE: + bgColor = COLORS.ERROR_DEFAULT; + break; + case COLORS.WARNING_INVERSE: + bgColor = COLORS.WARNING_DEFAULT; + break; + case COLORS.SUCCESS_INVERSE: + bgColor = COLORS.SUCCESS_DEFAULT; + break; + case COLORS.INFO_INVERSE: + bgColor = COLORS.INFO_DEFAULT; + break; + default: + bgColor = null; + break; + } + + return bgColor; +} + export const DefaultStory = (args) => ( -
    + + {args.children} + +); + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + children: 'The quick orange fox jumped over the lazy dog.', +}; + +export const Variant = (args) => ( + <> {Object.values(TYPOGRAPHY).map((variant) => ( -
    - {variant} -
    + + {args.children || variant} + ))} + +); + +export const Color = (args) => { + // Index of last valid color in ValidColors array + const LAST_VALID_COLORS_ARRAY_INDEX = 16; + return ( + <> + {Object.values(ValidColors).map((color, index) => { + if (index === LAST_VALID_COLORS_ARRAY_INDEX) { + return ( + + + DEPRECATED COLORS - DO NOT USE + + + {color} + + + ); + } else if (index >= LAST_VALID_COLORS_ARRAY_INDEX) { + return ( + + {color} + + ); + } + return ( + + {color} + + ); + })} + + ); +}; + +export const FontWeight = (args) => ( + <> + {Object.values(FONT_WEIGHT).map((weight) => ( + + {weight} + + ))} + +); + +export const FontStyle = (args) => ( + <> + {Object.values(FONT_STYLE).map((style) => ( + + {style} + + ))} + +); + +export const Align = (args) => ( + <> + {Object.values(TEXT_ALIGN).map((align) => ( + + {align} + + ))} + +); + +export const OverflowWrap = (args) => ( +
    + + {OVERFLOW_WRAP.NORMAL}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d + + + {OVERFLOW_WRAP.BREAK_WORD}: 0x39013f961c378f02c2b82a6e1d31e9812786fd9d +
    ); -DefaultStory.storyName = 'List'; - -export const TheQuickOrangeFox = (args) => ( -
    -
    - {args.content} -
    -
    +export const Tag = (args) => ( + <> + {Object.values(ValidTags).map((tag) => ( + + {tag} + + ))} + ); + +export const Margin = (args) => ( + + This Typography component has a margin of {args.margin * 4}px + +); + +Margin.args = { + margin: 4, +}; + +export const BoxProps = (args) => ( + This uses the boxProps prop +); + +BoxProps.args = { + color: COLORS.TEXT_DEFAULT, + boxProps: { + backgroundColor: COLORS.INFO_MUTED, + borderColor: COLORS.INFO_DEFAULT, + padding: 4, + borderRadius: 4, + }, +}; diff --git a/ui/components/ui/ui-components.scss b/ui/components/ui/ui-components.scss index c958581d7..0a3b51552 100644 --- a/ui/components/ui/ui-components.scss +++ b/ui/components/ui/ui-components.scss @@ -49,6 +49,7 @@ @import 'site-origin/index'; @import 'slider/index'; @import 'tabs/index'; +@import 'text-field/index'; @import 'toggle-button/index'; @import 'token-balance/index'; @import 'tooltip/index'; diff --git a/ui/css/base-styles.scss b/ui/css/base-styles.scss index 22f9acd31..3ab475169 100644 --- a/ui/css/base-styles.scss +++ b/ui/css/base-styles.scss @@ -6,7 +6,7 @@ html, body { - color: #4d4d4d; + color: var(--color-text-default); width: 100%; height: 100%; margin: 0; @@ -49,10 +49,10 @@ html { height: 100%; display: flex; flex-direction: column; - background: #f7f7f7; + background: var(--color-background-alternative); @media screen and (max-width: $break-small) { - background-color: var(--white); + background-color: var(--color-background-default); } } /* stylelint-enable */ @@ -89,32 +89,32 @@ input.form-control { padding-left: 10px; font-size: 14px; height: 40px; - border: 1px solid var(--alto); + border: 1px solid var(--color-border-muted); border-radius: 3px; width: 100%; &::-webkit-input-placeholder { font-weight: 100; - color: var(--dusty-gray); + color: var(--color-text-alternative); } &::-moz-placeholder { font-weight: 100; - color: var(--dusty-gray); + color: var(--color-text-alternative); } &:-ms-input-placeholder { font-weight: 100; - color: var(--dusty-gray); + color: var(--color-text-alternative); } &:-moz-placeholder { font-weight: 100; - color: var(--dusty-gray); + color: var(--color-text-alternative); } &--error { - border: 1px solid var(--monzo); + border: 1px solid var(--color-error-default); } } diff --git a/ui/css/design-system/colors.scss b/ui/css/design-system/colors.scss index ac21d9cc9..ec3e6fdf3 100644 --- a/ui/css/design-system/colors.scss +++ b/ui/css/design-system/colors.scss @@ -1,4 +1,56 @@ $color-map: ( + 'background-default': --color-background-default, + 'background-alternative': --color-background-alternative, + 'text-default': --color-text-default, + 'text-alternative': --color-text-alternative, + 'text-muted': --color-text-muted, + 'icon-default': --color-icon-default, + 'icon-muted': --color-icon-muted, + 'border-default': --color-border-default, + 'border-muted': --color-border-muted, + 'overlay-default': --color-overlay-default, + 'overlay-inverse': --color-overlay-inverse, + 'primary-default': --color-primary-default, + 'primary-alternative': --color-primary-alternative, + 'primary-muted': --color-primary-muted, + 'primary-inverse': --color-primary-inverse, + 'primary-disabled': --color-primary-disabled, + 'secondary-default': --color-secondary-default, + 'secondary-alternative': --color-secondary-alternative, + 'secondary-muted': --color-secondary-muted, + 'secondary-inverse': --color-secondary-inverse, + 'secondary-disabled': --color-secondary-disabled, + 'error-default': --color-error-default, + 'error-alternative': --color-error-alternative, + 'error-muted': --color-error-muted, + 'error-inverse': --color-error-inverse, + 'error-disabled': --color-error-disabled, + 'warning-default': --color-warning-default, + 'warning-alternative': --color-warning-alternative, + 'warning-muted': --color-warning-muted, + 'warning-inverse': --color-warning-inverse, + 'warning-disabled': --color-warning-disabled, + 'success-default': --color-success-default, + 'success-alternative': --color-success-alternative, + 'success-muted': --color-success-muted, + 'success-inverse': --color-success-inverse, + 'success-disabled': --color-success-disabled, + 'info-default': --color-info-default, + 'info-alternative': --color-info-alternative, + 'info-muted': --color-info-muted, + 'info-inverse': --color-info-inverse, + 'info-disabled': --color-info-disabled, + 'mainnet': --mainnet, + 'ropsten': --ropsten, + 'kovan': --kovan, + 'rinkeby': --rinkeby, + 'goerli': --goerli, + 'localhost': --localhost, + 'transparent': transparent, + 'flask-purple': --flask-purple, + /** + * !!! DEPRECATED DO NOT USE!!! + */ 'ui-1': --ui-1, 'ui-2': --ui-2, 'ui-3': --ui-3, @@ -22,13 +74,5 @@ $color-map: ( 'error-3': --error-3, 'success-1': --success-1, 'success-2': --success-2, - 'success-3': --success-3, - 'mainnet': --mainnet, - 'ropsten': --ropsten, - 'kovan': --kovan, - 'rinkeby': --rinkeby, - 'goerli': --goerli, - 'localhost': --localhost, - 'transparent': transparent, - 'flask-purple': --flask-purple + 'success-3': --success-3 ); diff --git a/ui/css/design-system/deprecated-colors.scss b/ui/css/design-system/deprecated-colors.scss index 82d953de5..c43c8ee3b 100644 --- a/ui/css/design-system/deprecated-colors.scss +++ b/ui/css/design-system/deprecated-colors.scss @@ -9,10 +9,4 @@ $malibu-blue: #7ac9fd; $alto: #dedede; $black: #000; $white: #fff; -$blue-lagoon: #038789; -$crimson: #e91550; -$purple: #690496; -$tulip-tree: #ebb33f; -$dodger-blue: #3099f2; $monzo: #d0021b; -$dusty-gray: #9b9b9b; diff --git a/ui/css/index.scss b/ui/css/index.scss index bbbc050c1..bf9249706 100644 --- a/ui/css/index.scss +++ b/ui/css/index.scss @@ -29,3 +29,4 @@ Third Party Library Styles */ @import '../../node_modules/react-tippy/dist/tippy'; +@import '../../node_modules/@metamask/design-tokens/src/css/design-tokens'; diff --git a/ui/ducks/app/app.js b/ui/ducks/app/app.js index 228b3eb5b..1f6081343 100644 --- a/ui/ducks/app/app.js +++ b/ui/ducks/app/app.js @@ -381,10 +381,10 @@ export default function reduceApp(state = {}, action) { ...appState, ledgerTransportStatus: action.value, }; - case actionConstants.SET_CURRENCY_INPUT_SWITCH: + case actionConstants.TOGGLE_CURRENCY_INPUT_SWITCH: return { ...appState, - sendInputCurrencySwitched: action.value, + sendInputCurrencySwitched: !appState.sendInputCurrencySwitched, }; default: return appState; @@ -434,6 +434,6 @@ export function getLedgerTransportStatus(state) { return state.appState.ledgerTransportStatus; } -export function toggleCurrencySwitch(value) { - return { type: actionConstants.SET_CURRENCY_INPUT_SWITCH, value }; +export function toggleCurrencySwitch() { + return { type: actionConstants.TOGGLE_CURRENCY_INPUT_SWITCH }; } diff --git a/ui/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/ducks/confirm-transaction/confirm-transaction.duck.js index 3652a7d0a..c05050f28 100644 --- a/ui/ducks/confirm-transaction/confirm-transaction.duck.js +++ b/ui/ducks/confirm-transaction/confirm-transaction.duck.js @@ -20,7 +20,7 @@ import { import { conversionUtil } from '../../../shared/modules/conversion.utils'; import { getAveragePriceEstimateInHexWEI } from '../../selectors/custom-gas'; -import { isEqualCaseInsensitive } from '../../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; // Actions const createActionType = (action) => `metamask/confirm-transaction/${action}`; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 7aa2e1220..dcf4b033f 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -14,9 +14,9 @@ import { import { updateTransaction } from '../../store/actions'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'; -import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; export default function reduceMetamask(state = {}, action) { const metamaskState = { diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index abd74a4dc..d4b4c1084 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -53,7 +53,8 @@ import { hideLoadingIndication, showConfTxPage, showLoadingIndication, - updateTransaction, + updateEditableParams, + updateTransactionGasFees, addPollingTokenToAppState, removePollingTokenFromAppState, isCollectibleOwner, @@ -78,7 +79,6 @@ import { isDefaultMetaMaskChain, isOriginContractAddress, isValidDomainName, - isEqualCaseInsensitive, } from '../../helpers/utils/util'; import { getGasEstimateType, @@ -102,6 +102,7 @@ import { import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction'; import { readAddressAsContract } from '../../../shared/modules/contract-utils'; import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; // typedefs /** * @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction @@ -1700,7 +1701,8 @@ export function signTransaction() { eip1559support ? eip1559OnlyTxParamsToUpdate : txParams, ), }; - dispatch(updateTransaction(editingTx)); + dispatch(updateEditableParams(id, editingTx.txParams)); + dispatch(updateTransactionGasFees(id, editingTx.txParams)); } else if (asset.type === ASSET_TYPES.TOKEN) { // When sending a token transaction we have to the token.transfer method // on the token contract to construct the transaction. This results in diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 741b1370c..8e901b9dc 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -95,6 +95,12 @@ describe('Send Slice', () => { jest .spyOn(Actions, 'isCollectibleOwner') .mockImplementation(() => Promise.resolve(true)); + jest.spyOn(Actions, 'updateEditableParams').mockImplementation(() => ({ + type: 'UPDATE_TRANSACTION_EDITABLE_PARAMS', + })); + jest + .spyOn(Actions, 'updateTransactionGasFees') + .mockImplementation(() => ({ type: 'UPDATE_TRANSACTION_GAS_FEES' })); }); describe('Reducers', () => { @@ -2070,10 +2076,13 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(5); - expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); - expect(actionResult[1].type).toStrictEqual('UPDATE_TRANSACTION_PARAMS'); - expect(actionResult[2].type).toStrictEqual('HIDE_LOADING_INDICATION'); + expect(actionResult).toHaveLength(2); + expect(actionResult[0].type).toStrictEqual( + 'UPDATE_TRANSACTION_EDITABLE_PARAMS', + ); + expect(actionResult[1].type).toStrictEqual( + 'UPDATE_TRANSACTION_GAS_FEES', + ); }); }); diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 02d442951..7d1dd91a5 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -18,7 +18,8 @@ import { setTradeTxId, stopPollingForQuotes, updateAndApproveTx, - updateTransaction, + updateSwapApprovalTransaction, + updateSwapTransaction, resetBackgroundSwapsState, setSwapsLiveness, setSwapsFeatureFlags, @@ -32,6 +33,7 @@ import { fetchSmartTransactionFees, estimateSmartTransactionsGas, cancelSmartTransaction, + getTransactions, } from '../../store/actions'; import { AWAITING_SIGNATURES_ROUTE, @@ -80,6 +82,7 @@ import { } from '../../../shared/constants/swaps'; import { TRANSACTION_TYPES, + TRANSACTION_STATUSES, SMART_TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; import { getGasFeeEstimates } from '../metamask/metamask'; @@ -198,9 +201,9 @@ const slice = createSlice({ state.customGas.fallBackPrice = action.payload; }, setCurrentSmartTransactionsError: (state, action) => { - const errorType = stxErrorTypes.includes(action.payload) + const errorType = Object.values(stxErrorTypes).includes(action.payload) ? action.payload - : stxErrorTypes[0]; + : stxErrorTypes.UNAVAILABLE; state.currentSmartTransactionsError = errorType; }, dismissCurrentSmartTransactionsErrorMessage: (state) => { @@ -553,12 +556,24 @@ export const fetchSwapsLivenessAndFeatureFlags = () => { let swapsLivenessForNetwork = { swapsFeatureIsLive: false, }; - const chainId = getCurrentChainId(getState()); + const state = getState(); + const chainId = getCurrentChainId(state); try { const swapsFeatureFlags = await fetchSwapsFeatureFlags(); await dispatch(setSwapsFeatureFlags(swapsFeatureFlags)); if (ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS.includes(chainId)) { await dispatch(fetchSmartTransactionsLiveness()); + const pendingTransactions = await getTransactions({ + searchCriteria: { + status: TRANSACTION_STATUSES.PENDING, + from: state.metamask?.selectedAddress, + }, + }); + if (pendingTransactions?.length > 0) { + dispatch( + setCurrentSmartTransactionsError(stxErrorTypes.REGULAR_TX_PENDING), + ); + } } swapsLivenessForNetwork = getSwapsLivenessForNetwork( swapsFeatureFlags, @@ -819,6 +834,7 @@ export const signAndSendSwapsSmartTransaction = ({ unsignedTransaction, metaMetricsEvent, history, + additionalTrackingParams, }) => { return async (dispatch, getState) => { dispatch(setSwapsSTXSubmitLoading(true)); @@ -870,6 +886,7 @@ export const signAndSendSwapsSmartTransaction = ({ stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, + ...additionalTrackingParams, }; metaMetricsEvent({ event: 'STX Swap Started', @@ -965,7 +982,11 @@ export const signAndSendSwapsSmartTransaction = ({ }; }; -export const signAndSendTransactions = (history, metaMetricsEvent) => { +export const signAndSendTransactions = ( + history, + metaMetricsEvent, + additionalTrackingParams, +) => { return async (dispatch, getState) => { const state = getState(); const chainId = getCurrentChainId(state); @@ -1078,6 +1099,9 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { }); const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); + const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled( + state, + ); const swapMetaData = { token_from: sourceTokenInfo.symbol, token_from_amount: String(swapTokenValue), @@ -1104,7 +1128,9 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: getHardwareWalletType(state), stx_enabled: smartTransactionsEnabled, + current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, + ...additionalTrackingParams, }; if (networkAndAccountSupports1559) { swapMetaData.max_fee_per_gas = maxFeePerGas; @@ -1155,15 +1181,10 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { ); await dispatch(setApproveTxId(approveTxMeta.id)); finalApproveTxMeta = await dispatch( - updateTransaction( - { - ...approveTxMeta, - estimatedBaseFee: decEstimatedBaseFee, - type: TRANSACTION_TYPES.SWAP_APPROVAL, - sourceTokenSymbol: sourceTokenInfo.symbol, - }, - true, - ), + updateSwapApprovalTransaction(approveTxMeta.id, { + type: TRANSACTION_TYPES.SWAP_APPROVAL, + sourceTokenSymbol: sourceTokenInfo.symbol, + }), ); try { await dispatch(updateAndApproveTx(finalApproveTxMeta, true)); @@ -1199,21 +1220,17 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { return; } const finalTradeTxMeta = await dispatch( - updateTransaction( - { - ...tradeTxMeta, - estimatedBaseFee: decEstimatedBaseFee, - sourceTokenSymbol: sourceTokenInfo.symbol, - destinationTokenSymbol: destinationTokenInfo.symbol, - type: TRANSACTION_TYPES.SWAP, - destinationTokenDecimals: destinationTokenInfo.decimals, - destinationTokenAddress: destinationTokenInfo.address, - swapMetaData, - swapTokenValue, - approvalTxId: finalApproveTxMeta?.id, - }, - true, - ), + updateSwapTransaction(tradeTxMeta.id, { + estimatedBaseFee: decEstimatedBaseFee, + sourceTokenSymbol: sourceTokenInfo.symbol, + destinationTokenSymbol: destinationTokenInfo.symbol, + type: TRANSACTION_TYPES.SWAP, + destinationTokenDecimals: destinationTokenInfo.decimals, + destinationTokenAddress: destinationTokenInfo.address, + swapMetaData, + swapTokenValue, + approvalTxId: finalApproveTxMeta?.id, + }), ); try { await dispatch(updateAndApproveTx(finalTradeTxMeta, true)); diff --git a/ui/ducks/swaps/swaps.test.js b/ui/ducks/swaps/swaps.test.js index 657ac1dab..231a6c0e1 100644 --- a/ui/ducks/swaps/swaps.test.js +++ b/ui/ducks/swaps/swaps.test.js @@ -15,6 +15,7 @@ jest.mock('../../store/actions.js', () => ({ setSwapsLiveness: jest.fn(), setSwapsFeatureFlags: jest.fn(), fetchSmartTransactionsLiveness: jest.fn(), + getTransactions: jest.fn(), })); const providerState = { diff --git a/ui/helpers/constants/design-system.js b/ui/helpers/constants/design-system.js index 89b0a3801..d27c43d1f 100644 --- a/ui/helpers/constants/design-system.js +++ b/ui/helpers/constants/design-system.js @@ -5,6 +5,57 @@ * match the plural form of the thing. e.g. COLORS, TYPOGRAPHY */ export const COLORS = { + BACKGROUND_DEFAULT: 'background-default', + BACKGROUND_ALTERNATIVE: 'background-alternative', + TEXT_DEFAULT: 'text-default', + TEXT_ALTERNATIVE: 'text-alternative', + TEXT_MUTED: 'text-muted', + ICON_DEFAULT: 'icon-default', + ICON_MUTED: 'icon-muted', + BORDER_DEFAULT: 'border-default', + BORDER_MUTED: 'border-muted', + OVERLAY_DEFAULT: 'overlay-default', + OVERLAY_INVERSE: 'overlay-inverse', + PRIMARY_DEFAULT: 'primary-default', + PRIMARY_ALTERNATIVE: 'primary-alternative', + PRIMARY_MUTED: 'primary-muted', + PRIMARY_INVERSE: 'primary-inverse', + PRIMARY_DISABLED: 'primary-disabled', + SECONDARY_DEFAULT: 'secondary-default', + SECONDARY_ALTERNATIVE: 'secondary-alternative', + SECONDARY_MUTED: 'secondary-muted', + SECONDARY_INVERSE: 'secondary-inverse', + SECONDARY_DISABLED: 'secondary-disabled', + ERROR_DEFAULT: 'error-default', + ERROR_ALTERNATIVE: 'error-alternative', + ERROR_MUTED: 'error-muted', + ERROR_INVERSE: 'error-inverse', + ERROR_DISABLED: 'error-disabled', + WARNING_DEFAULT: 'warning-default', + WARNING_ALTERNATIVE: 'warning-alternative', + WARNING_MUTED: 'warning-muted', + WARNING_INVERSE: 'warning-inverse', + WARNING_DISABLED: 'warning-disabled', + SUCCESS_DEFAULT: 'success-default', + SUCCESS_ALTERNATIVE: 'success-alternative', + SUCCESS_MUTED: 'success-muted', + SUCCESS_INVERSE: 'success-inverse', + SUCCESS_DISABLED: 'success-disabled', + INFO_DEFAULT: 'info-default', + INFO_ALTERNATIVE: 'info-alternative', + INFO_MUTED: 'info-muted', + INFO_INVERSE: 'info-inverse', + INFO_DISABLED: 'info-disabled', + MAINNET: 'mainnet', + ROPSTEN: 'ropsten', + KOVAN: 'kovan', + RINKEBY: 'rinkeby', + GOERLI: 'goerli', + TRANSPARENT: 'transparent', + LOCALHOST: 'localhost', + /** + * !!! DEPRECATED DO NOT USE!!! + */ UI1: 'ui-1', UI2: 'ui-2', UI3: 'ui-3', @@ -28,13 +79,6 @@ export const COLORS = { ALERT1: 'alert-1', ALERT2: 'alert-2', ALERT3: 'alert-3', - MAINNET: 'mainnet', - ROPSTEN: 'ropsten', - KOVAN: 'kovan', - RINKEBY: 'rinkeby', - GOERLI: 'goerli', - TRANSPARENT: 'transparent', - LOCALHOST: 'localhost', }; export const TYPOGRAPHY = { @@ -161,15 +205,6 @@ export const TEXT_ALIGN = { export const FONT_WEIGHT = { BOLD: 'bold', NORMAL: 'normal', - 100: 100, - 200: 200, - 300: 300, - 400: 400, - 500: 500, - 600: 600, - 700: 700, - 800: 800, - 900: 900, }; export const OVERFLOW_WRAP = { @@ -180,7 +215,6 @@ export const OVERFLOW_WRAP = { export const FONT_STYLE = { ITALIC: 'italic', NORMAL: 'normal', - OBLIQUE: 'oblique', }; export const SEVERITIES = { diff --git a/ui/helpers/constants/onboarding.js b/ui/helpers/constants/onboarding.js new file mode 100644 index 000000000..a637bac6c --- /dev/null +++ b/ui/helpers/constants/onboarding.js @@ -0,0 +1,6 @@ +const FIRST_TIME_FLOW_TYPES = { + IMPORT: 'import', + CREATE: 'create', +}; + +export { FIRST_TIME_FLOW_TYPES }; diff --git a/ui/helpers/constants/zendesk-url.js b/ui/helpers/constants/zendesk-url.js index d9d2e3d2b..010d54cfb 100644 --- a/ui/helpers/constants/zendesk-url.js +++ b/ui/helpers/constants/zendesk-url.js @@ -1,6 +1,16 @@ const ZENDESK_URLS = { + ADD_CUSTOM_TOKENS: + 'https://metamask.zendesk.com/hc/en-us/articles/360015489031', + ADD_MISSING_ACCOUNTS: + 'https://metamask.zendesk.com/hc/en-us/articles/360015489271', + IMPORT_ACCOUNTS: + 'https://metamask.zendesk.com/hc/en-us/articles/360015489331', TOKEN_SAFETY_PRACTICES: 'https://metamask.zendesk.com/hc/en-us/articles/4403988839451', + SECRET_RECOVERY_PHRASE: + 'https://metamask.zendesk.com/hc/en-us/articles/360060826432-What-is-a-Secret-Recovery-Phrase-and-how-to-keep-your-crypto-wallet-secure', + PASSWORD_ARTICLE: + 'https://metamask.zendesk.com/hc/en-us/articles/4404722782107', }; export default ZENDESK_URLS; diff --git a/ui/helpers/utils/gas.js b/ui/helpers/utils/gas.js index 9b2c06e89..7fa72dd95 100644 --- a/ui/helpers/utils/gas.js +++ b/ui/helpers/utils/gas.js @@ -1,9 +1,13 @@ +import { constant, times, uniq, zip } from 'lodash'; import BigNumber from 'bignumber.js'; import { addHexPrefix } from 'ethereumjs-util'; - import { GAS_RECOMMENDATIONS } from '../../../shared/constants/gas'; import { multiplyCurrencies } from '../../../shared/modules/conversion.utils'; -import { bnGreaterThan } from './util'; +import { + bnGreaterThan, + isNullish, + roundToDecimalPlacesRemovingExtraZeroes, +} from './util'; import { hexWEIToDecGWEI } from './conversions.util'; export const gasEstimateGreaterThanGasUsedPlusTenPercent = ( @@ -62,3 +66,43 @@ export function isMetamaskSuggestedGasEstimate(estimate) { GAS_RECOMMENDATIONS.LOW, ].includes(estimate); } + +/** + * Formats a singular gas fee or a range of gas fees by rounding them to the + * given precisions and then arranging them as a string. + * + * @param {string | [string, string] | null | undefined} feeOrFeeRange - The fee + * in GWEI or range of fees in GWEI. + * @param {object} options - The options. + * @param {number | [number, number]} options.precision - The precision(s) to + * use when formatting the fee(s). + * @returns A string which represents the formatted version of the fee or fee + * range. + */ +export function formatGasFeeOrFeeRange( + feeOrFeeRange, + { precision: precisionOrPrecisions = 2 } = {}, +) { + if ( + isNullish(feeOrFeeRange) || + (Array.isArray(feeOrFeeRange) && feeOrFeeRange.length === 0) + ) { + return null; + } + + const range = Array.isArray(feeOrFeeRange) + ? feeOrFeeRange.slice(0, 2) + : [feeOrFeeRange]; + const precisions = Array.isArray(precisionOrPrecisions) + ? precisionOrPrecisions.slice(0, 2) + : times(range.length, constant(precisionOrPrecisions)); + const formattedRange = uniq( + zip(range, precisions).map(([fee, precision]) => { + return precision === undefined + ? fee + : roundToDecimalPlacesRemovingExtraZeroes(fee, precision); + }), + ).join(' - '); + + return `${formattedRange} GWEI`; +} diff --git a/ui/helpers/utils/gas.test.js b/ui/helpers/utils/gas.test.js index 58aaaf38e..cf197afb7 100644 --- a/ui/helpers/utils/gas.test.js +++ b/ui/helpers/utils/gas.test.js @@ -3,6 +3,7 @@ import { PRIORITY_LEVELS } from '../../../shared/constants/gas'; import { addTenPercent, gasEstimateGreaterThanGasUsedPlusTenPercent, + formatGasFeeOrFeeRange, } from './gas'; describe('Gas utils', () => { @@ -47,4 +48,64 @@ describe('Gas utils', () => { expect(result).toBeUndefined(); }); }); + + describe('formatGasFeeOrFeeRange', () => { + describe('given a singular fee', () => { + it('should return a string "X GWEI" where X is the fee rounded to the given precision', () => { + expect(formatGasFeeOrFeeRange('23.43', { precision: 1 })).toStrictEqual( + '23.4 GWEI', + ); + }); + }); + + describe('given an array of two fees', () => { + describe('given a single precision', () => { + it('should return a string "X - Y GWEI" where X and Y are the fees rounded to the given precision', () => { + expect( + formatGasFeeOrFeeRange(['23.43', '83.9342'], { precision: 1 }), + ).toStrictEqual('23.4 - 83.9 GWEI'); + }); + }); + + describe('given two precisions', () => { + it('should return a string "X - Y GWEI" where X and Y are the fees rounded to the given precisions', () => { + expect( + formatGasFeeOrFeeRange(['23.43', '83.9342'], { precision: [1, 0] }), + ).toStrictEqual('23.4 - 84 GWEI'); + }); + }); + + describe('given more than two precisions', () => { + it('should ignore precisions past 2', () => { + expect( + formatGasFeeOrFeeRange(['23.43', '83.9342'], { + precision: [1, 0, 999], + }), + ).toStrictEqual('23.4 - 84 GWEI'); + }); + }); + }); + + describe('given an array of more than two fees', () => { + it('should ignore fees past two', () => { + expect( + formatGasFeeOrFeeRange(['23.43', '83.9342', '300.3414'], { + precision: 1, + }), + ).toStrictEqual('23.4 - 83.9 GWEI'); + }); + }); + + describe('if the fee is null', () => { + it('should return null', () => { + expect(formatGasFeeOrFeeRange(null, { precision: 1 })).toBeNull(); + }); + }); + + describe('if the fee is undefined', () => { + it('should return null', () => { + expect(formatGasFeeOrFeeRange(null, { precision: 1 })).toBeNull(); + }); + }); + }); }); diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js new file mode 100644 index 000000000..31550eb5c --- /dev/null +++ b/ui/helpers/utils/settings-search.js @@ -0,0 +1,452 @@ +/* eslint-disable require-unicode-regexp */ +import { + ALERTS_ROUTE, + ADVANCED_ROUTE, + SECURITY_ROUTE, + GENERAL_ROUTE, + ABOUT_US_ROUTE, + NETWORKS_ROUTE, + CONTACT_LIST_ROUTE, + EXPERIMENTAL_ROUTE, +} from '../constants/routes'; + +function showHideSettings(t, settings) { + if (!process.env.COLLECTIBLES_V1) { + return settings.filter( + (e) => + 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/helpers/utils/token-util.js b/ui/helpers/utils/token-util.js index 68431dde5..a75db6da6 100644 --- a/ui/helpers/utils/token-util.js +++ b/ui/helpers/utils/token-util.js @@ -4,8 +4,12 @@ import { conversionUtil, multiplyCurrencies, } from '../../../shared/modules/conversion.utils'; +import { getTokenStandardAndDetails } from '../../store/actions'; +import { ERC1155, ERC721 } from '../constants/common'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import * as util from './util'; import { formatCurrency } from './confirm-tx.util'; +import { getTransactionData } from './transactions.util'; const DEFAULT_SYMBOL = ''; @@ -212,3 +216,48 @@ export function getTokenFiatAmount( } return result; } + +export async function getAssetDetails( + tokenAddress, + currentUserAddress, + transactionData, + existingCollectibles, +) { + const tokenData = getTransactionData(transactionData); + if (!tokenData) { + throw new Error('Unable to detect valid token data'); + } + + const tokenId = getTokenValueParam(tokenData); + let tokenDetails; + try { + tokenDetails = await getTokenStandardAndDetails( + tokenAddress, + currentUserAddress, + tokenId, + ); + } catch (error) { + log.warn(error); + return {}; + } + + if (tokenDetails?.standard) { + const { standard } = tokenDetails; + if (standard === ERC721 || standard === ERC1155) { + const existingCollectible = existingCollectibles.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), + ); + + if (existingCollectible) { + return { + ...existingCollectible, + standard, + }; + } + } + // else if not a collectible already in state or standard === ERC20 just return tokenDetails as it contains all required data + return tokenDetails; + } + + return {}; +} diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 9cb8a6ed3..bcb064003 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -65,14 +65,6 @@ export function isDefaultMetaMaskChain(chainId) { return false; } -// Both inputs should be strings. This method is currently used to compare tokenAddress hex strings. -export function isEqualCaseInsensitive(value1, value2) { - if (typeof value1 !== 'string' || typeof value2 !== 'string') { - return false; - } - return value1.toLowerCase() === value2.toLowerCase(); -} - export function valuesFor(obj) { if (!obj) { return []; @@ -596,3 +588,14 @@ export function coinTypeToProtocolName(coinType) { } return slip44[coinType]?.name || undefined; } + +/** + * Tests "nullishness". Used to guard a section of a component from being + * rendered based on a value. + * + * @param {any} value - A value (literally anything). + * @returns `true` if the value is null or undefined, `false` otherwise. + */ +export function isNullish(value) { + return value === null || value === undefined; +} 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/hooks/useAssetDetails.js b/ui/hooks/useAssetDetails.js new file mode 100644 index 000000000..ac12ed494 --- /dev/null +++ b/ui/hooks/useAssetDetails.js @@ -0,0 +1,118 @@ +import { useState, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { getCollectibles, getTokens } from '../ducks/metamask/metamask'; +import { ERC1155, ERC721, ERC20 } from '../helpers/constants/common'; +import { + calcTokenAmount, + getAssetDetails, + getTokenAddressParam, + getTokenValueParam, +} from '../helpers/utils/token-util'; +import { getTransactionData } from '../helpers/utils/transactions.util'; +import { getTokenList } from '../selectors'; +import { hideLoadingIndication, showLoadingIndication } from '../store/actions'; +import { usePrevious } from './usePrevious'; + +export function useAssetDetails(tokenAddress, userAddress, transactionData) { + const dispatch = useDispatch(); + + // state selectors + const tokens = useSelector(getTokens); + const collectibles = useSelector(getCollectibles); + const tokenList = useSelector(getTokenList); + + // in-hook state + const [currentAsset, setCurrentAsset] = useState(null); + + // previous state checkers + const prevTokenAddress = usePrevious(tokenAddress); + const prevUserAddress = usePrevious(userAddress); + const prevTransactionData = usePrevious(transactionData); + + useEffect(() => { + async function getAndSetAssetDetails() { + dispatch(showLoadingIndication()); + const assetDetails = await getAssetDetails( + tokenAddress, + userAddress, + transactionData, + collectibles, + tokens, + tokenList, + ); + setCurrentAsset(assetDetails); + dispatch(hideLoadingIndication()); + } + if ( + tokenAddress !== prevTokenAddress || + userAddress !== prevUserAddress || + transactionData !== prevTransactionData + ) { + getAndSetAssetDetails(); + } + }, [ + dispatch, + prevTokenAddress, + prevTransactionData, + prevUserAddress, + tokenAddress, + userAddress, + transactionData, + collectibles, + tokens, + tokenList, + ]); + + let assetStandard, + assetName, + assetAddress, + tokenSymbol, + decimals, + tokenImage, + userBalance, + tokenValue, + toAddress, + tokenAmount, + tokenId; + + if (currentAsset) { + const { + standard, + symbol, + image, + name, + balance, + decimals: currentAssetDecimals, + } = currentAsset; + const tokenData = getTransactionData(transactionData); + assetStandard = standard; + assetAddress = tokenAddress; + tokenSymbol = symbol; + tokenImage = image; + toAddress = getTokenAddressParam(tokenData); + if (assetStandard === ERC721 || assetStandard === ERC1155) { + assetName = name; + tokenId = getTokenValueParam(tokenData); + } + if (assetStandard === ERC20) { + userBalance = balance; + decimals = Number(currentAssetDecimals?.toString(10)); + tokenAmount = + tokenData && + calcTokenAmount(getTokenValueParam(tokenData), decimals).toString(10); + } + } + return { + assetStandard, + assetName, + assetAddress, + userBalance, + tokenSymbol, + decimals, + tokenImage, + tokenValue, + toAddress, + tokenAmount, + tokenId, + }; +} diff --git a/ui/hooks/useCurrentAsset.js b/ui/hooks/useCurrentAsset.js index f98fcab82..97fa27fc1 100644 --- a/ui/hooks/useCurrentAsset.js +++ b/ui/hooks/useCurrentAsset.js @@ -3,11 +3,11 @@ import { useRouteMatch } from 'react-router-dom'; import { getTokens } from '../ducks/metamask/metamask'; import { getCurrentChainId } from '../selectors'; import { ASSET_ROUTE } from '../helpers/constants/routes'; -import { isEqualCaseInsensitive } from '../helpers/utils/util'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, ETH_SWAPS_TOKEN_OBJECT, } from '../../shared/constants/swaps'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; /** * Returns a token object for the asset that is currently being viewed. diff --git a/ui/hooks/useTokenFiatAmount.js b/ui/hooks/useTokenFiatAmount.js index 9fe9c816f..4565bc4f6 100644 --- a/ui/hooks/useTokenFiatAmount.js +++ b/ui/hooks/useTokenFiatAmount.js @@ -7,7 +7,7 @@ import { } from '../selectors'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { getConversionRate } from '../ducks/metamask/metamask'; -import { isEqualCaseInsensitive } from '../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; /** * Get the token balance converted to fiat and formatted for display diff --git a/ui/hooks/useTokenTracker.js b/ui/hooks/useTokenTracker.js index 29772a1de..b58562168 100644 --- a/ui/hooks/useTokenTracker.js +++ b/ui/hooks/useTokenTracker.js @@ -3,7 +3,7 @@ import TokenTracker from '@metamask/eth-token-tracker'; import { shallowEqual, useSelector } from 'react-redux'; import { getCurrentChainId, getSelectedAddress } from '../selectors'; import { SECOND } from '../../shared/constants/time'; -import { isEqualCaseInsensitive } from '../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { useEqualityCheck } from './useEqualityCheck'; export function useTokenTracker( diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index e12c75b9f..a56181e03 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -11,7 +11,6 @@ import { getTokenValueParam, } from '../helpers/utils/token-util'; import { - isEqualCaseInsensitive, formatDateWithYearContext, shortenAddress, stripHttpSchemes, @@ -28,6 +27,7 @@ import { TRANSACTION_STATUSES, } from '../../shared/constants/transaction'; import { captureSingleException } from '../store/actions'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { useI18nContext } from './useI18nContext'; import { useTokenFiatAmount } from './useTokenFiatAmount'; import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; diff --git a/ui/pages/add-collectible/add-collectible.js b/ui/pages/add-collectible/add-collectible.js index 188756aa1..bea9d2046 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')} + + @@ -204,13 +216,4 @@ const ConfirmAddSuggestedToken = (props) => { ); }; -ConfirmAddSuggestedToken.propTypes = { - acceptWatchAsset: PropTypes.func, - history: PropTypes.object, - mostRecentOverviewPage: PropTypes.string.isRequired, - rejectWatchAsset: PropTypes.func, - suggestedAssets: PropTypes.array, - tokens: PropTypes.array, -}; - export default ConfirmAddSuggestedToken; diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js index c5cf1f5e4..f984a50bb 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js @@ -10,7 +10,6 @@ export default { title: 'Pages/ConfirmAddSuggestedToken', id: __filename, argTypes: { - // Data tokens: { control: 'array', table: { category: 'Data' }, @@ -19,26 +18,6 @@ export default { control: 'array', table: { category: 'Data' }, }, - - // Text - mostRecentOverviewPage: { - control: { type: 'text', disable: true }, - table: { category: 'Text' }, - }, - - // Events - acceptWatchAsset: { - action: 'acceptWatchAsset', - table: { category: 'Events' }, - }, - history: { - action: 'history', - table: { category: 'Events' }, - }, - rejectWatchAsset: { - action: 'rejectWatchAsset', - table: { category: 'Events' }, - }, }, }; diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js new file mode 100644 index 000000000..fc35965fd --- /dev/null +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js @@ -0,0 +1,142 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { acceptWatchAsset, rejectWatchAsset } from '../../store/actions'; +import configureStore from '../../store/store'; +import { renderWithProvider } from '../../../test/jest/rendering'; +import ConfirmAddSuggestedToken from '.'; + +const MOCK_SUGGESTED_ASSETS = [ + { + id: 1, + asset: { + address: '0x8b175474e89094c44da98b954eedeac495271d0a', + symbol: 'NEW', + decimals: 18, + image: 'metamark.svg', + unlisted: false, + }, + }, + { + id: 2, + asset: { + address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51', + symbol: '0XYX', + decimals: 18, + image: '0x.svg', + unlisted: false, + }, + }, +]; + +const MOCK_TOKEN = { + address: '0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d', + symbol: 'TEST', + decimals: '0', +}; + +jest.mock('../../store/actions', () => ({ + acceptWatchAsset: jest.fn().mockReturnValue({ type: 'test' }), + rejectWatchAsset: jest.fn().mockReturnValue({ type: 'test' }), +})); + +const renderComponent = (tokens = []) => { + const store = configureStore({ + metamask: { + suggestedAssets: [...MOCK_SUGGESTED_ASSETS], + tokens, + provider: { chainId: '0x1' }, + }, + history: { + mostRecentOverviewPage: '/', + }, + }); + return renderWithProvider(, store); +}; + +describe('ConfirmAddSuggestedToken Component', () => { + it('should render', () => { + renderComponent(); + + expect(screen.getByText('Add Suggested Tokens')).toBeInTheDocument(); + expect( + screen.getByText('Would you like to import these tokens?'), + ).toBeInTheDocument(); + expect(screen.getByText('Token')).toBeInTheDocument(); + expect(screen.getByText('Balance')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Add Token' }), + ).toBeInTheDocument(); + }); + + it('should render the list of suggested tokens', () => { + renderComponent(); + + for (const { asset } of MOCK_SUGGESTED_ASSETS) { + expect(screen.getByText(asset.symbol)).toBeInTheDocument(); + } + expect(screen.getAllByRole('img')).toHaveLength( + MOCK_SUGGESTED_ASSETS.length, + ); + }); + + it('should dispatch acceptWatchAsset when clicking the "Add Token" button', () => { + renderComponent(); + const addTokenBtn = screen.getByRole('button', { name: 'Add Token' }); + + fireEvent.click(addTokenBtn); + expect(acceptWatchAsset).toHaveBeenCalled(); + }); + + it('should dispatch rejectWatchAsset when clicking the "Cancel" button', () => { + renderComponent(); + const cancelBtn = screen.getByRole('button', { name: 'Cancel' }); + + expect(rejectWatchAsset).toHaveBeenCalledTimes(0); + fireEvent.click(cancelBtn); + expect(rejectWatchAsset).toHaveBeenCalledTimes( + MOCK_SUGGESTED_ASSETS.length, + ); + }); + + describe('when the suggested token address matches an existing token address', () => { + it('should show "already listed" warning', () => { + const mockTokens = [ + { + ...MOCK_TOKEN, + address: MOCK_SUGGESTED_ASSETS[0].asset.address, + }, + ]; + renderComponent(mockTokens); + + expect( + screen.getByText( + 'This action will edit tokens that are already listed in your wallet, which can be used' + + ' to phish you. Only approve if you are certain that you mean to change what these' + + ' tokens represent. Learn more about', + ), + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: 'scams and security risks.' }), + ).toBeInTheDocument(); + }); + }); + + describe('when the suggested token symbol matches an existing token symbol and has a different address', () => { + it('should show "reuses a symbol" warning', () => { + const mockTokens = [ + { + ...MOCK_TOKEN, + symbol: MOCK_SUGGESTED_ASSETS[0].asset.symbol, + }, + ]; + renderComponent(mockTokens); + + expect( + screen.getByText( + 'A token here reuses a symbol from another token you watch, this can be confusing or deceptive.', + ), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/pages/confirm-add-suggested-token/index.js b/ui/pages/confirm-add-suggested-token/index.js index e04fe003b..0711408c9 100644 --- a/ui/pages/confirm-add-suggested-token/index.js +++ b/ui/pages/confirm-add-suggested-token/index.js @@ -1,3 +1 @@ -import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container'; - -export default ConfirmAddSuggestedToken; +export { default } from './confirm-add-suggested-token'; diff --git a/ui/pages/confirm-add-suggested-token/index.scss b/ui/pages/confirm-add-suggested-token/index.scss index 63ee8c657..714e47286 100644 --- a/ui/pages/confirm-add-suggested-token/index.scss +++ b/ui/pages/confirm-add-suggested-token/index.scss @@ -1,4 +1,6 @@ .confirm-add-suggested-token { + padding: 16px; + &__link { @include H7; @@ -6,4 +8,51 @@ color: var(--primary-blue); padding-left: 0; } + + &__header { + @include H7; + + display: flex; + } + + &__token { + flex: 1; + min-width: 0; + } + + &__balance { + flex: 0 0 30%; + min-width: 0; + } + + &__token-list { + display: flex; + flex-flow: column nowrap; + } + + &__token-list-item { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-top: 8px; + box-sizing: border-box; + } + + &__data { + display: flex; + align-items: center; + padding: 8px; + } + + &__name { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__token-icon { + margin-right: 12px; + flex: 0 0 auto; + } } diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index e1ab67009..1f349d6ad 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -28,6 +28,7 @@ import { SECOND } from '../../../../shared/constants/time'; import { ConfirmPageContainerWarning } from '../../../components/app/confirm-page-container/confirm-page-container-content'; import GasDetailsItem from '../../../components/app/gas-details-item'; import LedgerInstructionField from '../../../components/app/ledger-instruction-field'; +import { ERC1155, ERC20, ERC721 } from '../../../helpers/constants/common'; export default class ConfirmApproveContent extends Component { static contextTypes = { @@ -60,13 +61,15 @@ export default class ConfirmApproveContent extends Component { warning: PropTypes.string, txData: PropTypes.object, fromAddressIsLedger: PropTypes.bool, - tokenImage: PropTypes.string, chainId: PropTypes.string, rpcPrefs: PropTypes.object, isContract: PropTypes.bool, hexTransactionTotal: PropTypes.string, isMultiLayerFeeNetwork: PropTypes.bool, supportsEIP1559V2: PropTypes.bool, + assetName: PropTypes.string, + tokenId: PropTypes.string, + assetStandard: PropTypes.string, }; state = { @@ -178,7 +181,60 @@ export default class ConfirmApproveContent extends Component { ); } - renderPermissionContent() { + renderERC721OrERC1155PermissionContent() { + const { t } = this.context; + const { origin, toAddress, isContract, assetName, tokenId } = this.props; + + const displayedAddress = isContract + ? `${t('contract')} (${addressSummary(toAddress)})` + : addressSummary(toAddress); + return ( +
    +
    + {t('accessAndSpendNoticeNFT', [origin])} +
    +
    +
    + {t('approvedAsset')}: +
    +
    + {`${assetName} #${tokenId}`} +
    +
    +
    +
    + {t('grantedToWithColon')} +
    +
    + {displayedAddress} +
    +
    + +
    +
    +
    + ); + } + + renderERC20PermissionContent() { const { t } = this.context; const { customTokenAmount, @@ -188,6 +244,7 @@ export default class ConfirmApproveContent extends Component { toAddress, isContract, } = this.props; + const displayedAddress = isContract ? `${t('contract')} (${addressSummary(toAddress)})` : addressSummary(toAddress); @@ -252,6 +309,75 @@ export default class ConfirmApproveContent extends Component { ); } + renderFullDetails() { + const { t } = this.context; + const { + assetStandard, + showEditApprovalPermissionModal, + customTokenAmount, + tokenAmount, + decimals, + origin, + setCustomAmount, + tokenSymbol, + tokenBalance, + } = this.props; + if (assetStandard === ERC20) { + return ( +
    +
    + {this.renderApproveContentCard({ + symbol: , + title: t('permissionRequest'), + content: this.renderERC20PermissionContent(), + showEdit: true, + onEditClick: () => + showEditApprovalPermissionModal({ + customTokenAmount, + decimals, + origin, + setCustomAmount, + tokenAmount, + tokenSymbol, + tokenBalance, + }), + })} +
    +
    + {this.renderApproveContentCard({ + symbol: , + title: 'Data', + content: this.renderDataContent(), + noBorder: true, + })} +
    +
    + ); + } else if (assetStandard === ERC721 || assetStandard === ERC1155) { + return ( +
    +
    + {this.renderApproveContentCard({ + symbol: , + title: t('permissionRequest'), + content: this.renderERC721OrERC1155PermissionContent(), + showEdit: false, + })} +
    +
    + {this.renderApproveContentCard({ + symbol: , + title: t('data'), + content: this.renderDataContent(), + noBorder: true, + })} +
    +
    + ); + } + return null; + } + renderCustomNonceContent() { const { t } = this.context; const { @@ -321,11 +447,13 @@ export default class ConfirmApproveContent extends Component { warning, txData, fromAddressIsLedger, - tokenImage, toAddress, chainId, rpcPrefs, isContract, + assetStandard, + tokenId, + assetName, } = this.props; const { showFullTxDetails } = this.state; @@ -360,7 +488,7 @@ export default class ConfirmApproveContent extends Component { {getURLHostName(origin)} @@ -368,7 +496,11 @@ export default class ConfirmApproveContent extends Component {
    - {t('allowSpendToken', [tokenSymbol])} + {t('allowSpendToken', [ + assetStandard === ERC20 + ? tokenSymbol + : `${assetName} (#${tokenId})`, + ])}
    {t('trustSiteApprovePermission', [ @@ -383,12 +515,11 @@ export default class ConfirmApproveContent extends Component { className="confirm-approve-content__address-identicon" diameter={20} address={toAddress} - image={tokenImage} /> {ellipsify(toAddress)} @@ -438,24 +569,26 @@ export default class ConfirmApproveContent extends Component { -
    -
    - showEditApprovalPermissionModal({ - customTokenAmount, - decimals, - origin, - setCustomAmount, - tokenAmount, - tokenSymbol, - tokenBalance, - }) - } - > - {t('editPermission')} + {assetStandard === ERC20 ? ( +
    +
    + showEditApprovalPermissionModal({ + customTokenAmount, + decimals, + origin, + setCustomAmount, + tokenAmount, + tokenSymbol, + tokenBalance, + }) + } + > + {t('editPermission')} +
    -
    + ) : null}
    {this.renderApproveContentCard({ symbol: , @@ -527,36 +660,7 @@ export default class ConfirmApproveContent extends Component {
    ) : null} - {showFullTxDetails ? ( -
    -
    - {this.renderApproveContentCard({ - symbol: , - title: t('permissionRequest'), - content: this.renderPermissionContent(), - showEdit: true, - onEditClick: () => - showEditApprovalPermissionModal({ - customTokenAmount, - decimals, - origin, - setCustomAmount, - tokenAmount, - tokenSymbol, - tokenBalance, - }), - })} -
    -
    - {this.renderApproveContentCard({ - symbol: , - title: 'Data', - content: this.renderDataContent(), - noBorder: true, - })} -
    -
    - ) : null} + {showFullTxDetails ? this.renderFullDetails() : null}
    ); } diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 162332853..52ecdeece 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -2,6 +2,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { fireEvent } from '@testing-library/react'; import { renderWithProvider } from '../../../../test/jest/rendering'; +import { ERC20 } from '../../../helpers/constants/common'; import ConfirmApproveContent from '.'; const renderComponent = (props) => { @@ -16,6 +17,7 @@ const props = { tokenAmount: '10', origin: 'https://metamask.github.io/test-dapp/', tokenSymbol: 'TST', + assetStandard: ERC20, tokenImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg', tokenBalance: '15', showCustomizeGasModal: jest.fn(), diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js index 5ba090fed..246baca77 100644 --- a/ui/pages/confirm-approve/confirm-approve.js +++ b/ui/pages/confirm-approve/confirm-approve.js @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; import ConfirmTransactionBase from '../confirm-transaction-base'; import { EDIT_GAS_MODES } from '../../../shared/constants/gas'; import { @@ -8,24 +8,15 @@ import { updateCustomNonce, getNextNonce, } from '../../store/actions'; -import { getTransactionData } from '../../helpers/utils/transactions.util'; -import { - calcTokenAmount, - getTokenAddressParam, - getTokenValueParam, -} from '../../helpers/utils/token-util'; +import { calcTokenAmount } from '../../helpers/utils/token-util'; import { readAddressAsContract } from '../../../shared/modules/contract-utils'; import { GasFeeContextProvider } from '../../contexts/gasFee'; import { TransactionModalContextProvider } from '../../contexts/transaction-modal'; -import { useTokenTracker } from '../../hooks/useTokenTracker'; import { - getTokens, getNativeCurrency, isAddressLedger, } from '../../ducks/metamask/metamask'; import { - transactionFeeSelector, - txDataSelector, getCurrentCurrency, getSubjectMetadata, getUseNonceField, @@ -38,12 +29,11 @@ import { getEIP1559V2Enabled, } from '../../selectors'; import { useApproveTransaction } from '../../hooks/useApproveTransaction'; -import { currentNetworkTxListSelector } from '../../selectors/transactions'; import AdvancedGasFeePopover from '../../components/app/advanced-gas-fee-popover'; import EditGasFeePopover from '../../components/app/edit-gas-fee-popover'; import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component'; import Loading from '../../components/ui/loading-screen'; -import { isEqualCaseInsensitive } from '../../helpers/utils/util'; +import { ERC20, ERC1155, ERC721 } from '../../helpers/constants/common'; import { getCustomTxParamsData } from './confirm-approve.util'; import ConfirmApproveContent from './confirm-approve-content'; @@ -51,19 +41,28 @@ const isAddressLedgerByFromAddress = (address) => (state) => { return isAddressLedger(state, address); }; -export default function ConfirmApprove() { +export default function ConfirmApprove({ + assetStandard, + assetName, + userBalance, + tokenSymbol, + decimals, + tokenImage, + tokenAmount, + tokenId, + userAddress, + toAddress, + transaction, + ethTransactionTotal, + fiatTransactionTotal, + hexTransactionTotal, +}) { const dispatch = useDispatch(); - const { id: paramsTransactionId } = useParams(); - const { - id: transactionId, - txParams: { to: tokenAddress, data, from } = {}, - } = useSelector(txDataSelector); + const { txParams: { data: transactionData } = {} } = transaction; const currentCurrency = useSelector(getCurrentCurrency); const nativeCurrency = useSelector(getNativeCurrency); - const currentNetworkTxList = useSelector(currentNetworkTxListSelector); const subjectMetadata = useSelector(getSubjectMetadata); - const tokens = useSelector(getTokens); const useNonceField = useSelector(getUseNonceField); const nextNonce = useSelector(getNextSuggestedNonce); const customNonceValue = useSelector(getCustomNonceValue); @@ -73,45 +72,17 @@ export default function ConfirmApprove() { const networkAndAccountSupports1559 = useSelector( checkNetworkAndAccountSupports1559, ); - - const fromAddressIsLedger = useSelector(isAddressLedgerByFromAddress(from)); - - const transaction = - currentNetworkTxList.find( - ({ id }) => id === (Number(paramsTransactionId) || transactionId), - ) || {}; - const { - ethTransactionTotal, - fiatTransactionTotal, - hexTransactionTotal, - } = useSelector((state) => transactionFeeSelector(state, transaction)); + const fromAddressIsLedger = useSelector( + isAddressLedgerByFromAddress(userAddress), + ); + const [customPermissionAmount, setCustomPermissionAmount] = useState(''); + const [submitWarning, setSubmitWarning] = useState(''); + const [isContract, setIsContract] = useState(false); const eip1559V2Enabled = useSelector(getEIP1559V2Enabled); const supportsEIP1559V2 = eip1559V2Enabled && networkAndAccountSupports1559; - const currentToken = (tokens && - tokens.find(({ address }) => - isEqualCaseInsensitive(tokenAddress, address), - )) || { - address: tokenAddress, - }; - - const { tokensWithBalances } = useTokenTracker([currentToken]); - const tokenTrackerBalance = tokensWithBalances[0]?.balance || ''; - - const tokenSymbol = currentToken?.symbol; - const decimals = Number(currentToken?.decimals); - const tokenImage = currentToken?.image; - const tokenData = getTransactionData(data); - const tokenValue = getTokenValueParam(tokenData); - const toAddress = getTokenAddressParam(tokenData); - const tokenAmount = - tokenData && calcTokenAmount(tokenValue, decimals).toString(10); - - const [customPermissionAmount, setCustomPermissionAmount] = useState(''); - const previousTokenAmount = useRef(tokenAmount); - const { approveTransaction, showCustomizeGasPopover, @@ -125,7 +96,6 @@ export default function ConfirmApprove() { previousTokenAmount.current = tokenAmount; }, [customPermissionAmount, tokenAmount]); - const [submitWarning, setSubmitWarning] = useState(''); const prevNonce = useRef(nextNonce); const prevCustomNonce = useRef(customNonceValue); useEffect(() => { @@ -145,7 +115,6 @@ export default function ConfirmApprove() { prevNonce.current = nextNonce; }, [customNonceValue, nextNonce]); - const [isContract, setIsContract] = useState(false); const checkIfContract = useCallback(async () => { const { isContractAddress } = await readAddressAsContract( global.eth, @@ -153,6 +122,7 @@ export default function ConfirmApprove() { ); setIsContract(isContractAddress); }, [setIsContract, toAddress]); + useEffect(() => { checkIfContract(); }, [checkIfContract]); @@ -162,21 +132,30 @@ export default function ConfirmApprove() { const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {}; - const tokensText = `${Number(tokenAmount)} ${tokenSymbol}`; - const tokenBalance = tokenTrackerBalance - ? calcTokenAmount(tokenTrackerBalance, decimals).toString(10) + let tokensText; + if (assetStandard === ERC20) { + tokensText = `${Number(tokenAmount)} ${tokenSymbol}`; + } else if (assetStandard === ERC721 || assetStandard === ERC1155) { + tokensText = assetName; + } + + const tokenBalance = userBalance + ? calcTokenAmount(userBalance, decimals).toString(10) : ''; const customData = customPermissionAmount - ? getCustomTxParamsData(data, { customPermissionAmount, decimals }) + ? getCustomTxParamsData(transactionData, { + customPermissionAmount, + decimals, + }) : null; - return tokenSymbol === undefined ? ( + return tokenSymbol === undefined && assetName === undefined ? ( ) : ( ); } + +ConfirmApprove.propTypes = { + assetStandard: PropTypes.string, + assetName: PropTypes.string, + userBalance: PropTypes.string, + tokenSymbol: PropTypes.string, + decimals: PropTypes.string, + tokenImage: PropTypes.string, + tokenAmount: PropTypes.string, + tokenId: PropTypes.string, + userAddress: PropTypes.string, + toAddress: PropTypes.string, + transaction: PropTypes.shape({ + origin: PropTypes.string, + txParams: PropTypes.shape({ + data: PropTypes.string, + to: PropTypes.string, + from: PropTypes.string, + }), + }), + ethTransactionTotal: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + hexTransactionTotal: PropTypes.string, +}; diff --git a/ui/pages/confirm-import-token/confirm-import-token.js b/ui/pages/confirm-import-token/confirm-import-token.js index dcba139b2..07ab83ef9 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) { @@ -121,7 +113,10 @@ const ConfirmImportToken = () => { type="secondary" large className="page-container__footer-button" - onClick={() => history.push(IMPORT_TOKEN_ROUTE)} + onClick={() => { + dispatch(clearPendingTokens()); + history.push(IMPORT_TOKEN_ROUTE); + }} > {t('back')} diff --git a/ui/pages/confirm-import-token/index.scss b/ui/pages/confirm-import-token/index.scss index b7429f9c6..4dbc659ec 100644 --- a/ui/pages/confirm-import-token/index.scss +++ b/ui/pages/confirm-import-token/index.scss @@ -20,25 +20,6 @@ &__token-list { display: flex; flex-flow: column nowrap; - - .token-balance { - display: flex; - flex-flow: row nowrap; - align-items: flex-start; - - &__amount { - @include H1; - - color: var(--scorpion); - margin-right: 8px; - } - - &__symbol { - @include H5; - - color: var(--scorpion); - } - } } &__token-list-item { diff --git a/ui/pages/confirm-send-token/confirm-send-token.js b/ui/pages/confirm-send-token/confirm-send-token.js new file mode 100644 index 000000000..a03fb9835 --- /dev/null +++ b/ui/pages/confirm-send-token/confirm-send-token.js @@ -0,0 +1,118 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base/confirm-token-transaction-base'; +import { SEND_ROUTE } from '../../helpers/constants/routes'; +import { ASSET_TYPES, editTransaction } from '../../ducks/send'; +import { + contractExchangeRateSelector, + getCurrentCurrency, +} from '../../selectors'; +import { + getConversionRate, + getNativeCurrency, +} from '../../ducks/metamask/metamask'; +import { ERC20, ERC721 } from '../../helpers/constants/common'; +import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; +import { showSendTokenPage } from '../../store/actions'; + +export default function ConfirmSendToken({ + assetStandard, + toAddress, + tokenAddress, + assetName, + tokenSymbol, + tokenAmount, + tokenId, + transaction, + image, + ethTransactionTotal, + fiatTransactionTotal, + hexMaximumTransactionFee, +}) { + const dispatch = useDispatch(); + const history = useHistory(); + + const handleEditTransaction = ({ + txData, + tokenData, + tokenProps: assetDetails, + }) => { + const { id } = txData; + dispatch( + editTransaction( + ASSET_TYPES.TOKEN, + id.toString(), + tokenData, + assetDetails, + ), + ); + dispatch(clearConfirmTransaction()); + dispatch(showSendTokenPage()); + }; + + const handleEdit = (confirmTransactionData) => { + handleEditTransaction(confirmTransactionData); + history.push(SEND_ROUTE); + }; + const conversionRate = useSelector(getConversionRate); + const nativeCurrency = useSelector(getNativeCurrency); + const currentCurrency = useSelector(getCurrentCurrency); + const contractExchangeRate = useSelector(contractExchangeRateSelector); + + let title, subtitle; + + if (assetStandard === ERC721) { + title = assetName; + subtitle = `#${tokenId}`; + } else if (assetStandard === ERC20) { + title = `${tokenAmount} ${tokenSymbol}`; + } + + return ( + + ); +} + +ConfirmSendToken.propTypes = { + tokenAmount: PropTypes.string, + assetStandard: PropTypes.string, + assetName: PropTypes.string, + tokenSymbol: PropTypes.string, + image: PropTypes.string, + tokenId: PropTypes.string, + toAddress: PropTypes.string, + tokenAddress: PropTypes.string, + transaction: PropTypes.shape({ + origin: PropTypes.string, + txParams: PropTypes.shape({ + data: PropTypes.string, + to: PropTypes.string, + from: PropTypes.string, + }), + }), + ethTransactionTotal: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + hexMaximumTransactionFee: PropTypes.string, +}; diff --git a/ui/pages/confirm-send-token/index.js b/ui/pages/confirm-send-token/index.js index 25067ca43..3bf86d319 100644 --- a/ui/pages/confirm-send-token/index.js +++ b/ui/pages/confirm-send-token/index.js @@ -1 +1 @@ -export { default } from './confirm-send-token.container'; +export { default } from './confirm-send-token'; diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js deleted file mode 100644 index dbbc5cbca..000000000 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js +++ /dev/null @@ -1,112 +0,0 @@ -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { withRouter } from 'react-router-dom'; -import { - contractExchangeRateSelector, - transactionFeeSelector, -} from '../../selectors'; -import { getCollectibles, getTokens } from '../../ducks/metamask/metamask'; -import { getTransactionData } from '../../helpers/utils/transactions.util'; -import { - calcTokenAmount, - getTokenAddressParam, - getTokenValueParam, -} from '../../helpers/utils/token-util'; -import { hexWEIToDecETH } from '../../helpers/utils/conversions.util'; -import { isEqualCaseInsensitive } from '../../helpers/utils/util'; -import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'; - -const mapStateToProps = (state, ownProps) => { - const { - match: { params = {} }, - } = ownProps; - const { id: paramsTransactionId } = params; - const { - confirmTransaction, - metamask: { - currentCurrency, - conversionRate, - currentNetworkTxList, - nativeCurrency, - }, - } = state; - - const { - txData: { - id: transactionId, - txParams: { to: tokenAddress, data } = {}, - } = {}, - } = confirmTransaction; - - const transaction = - currentNetworkTxList.find( - ({ id }) => id === (Number(paramsTransactionId) || transactionId), - ) || {}; - - const { - ethTransactionTotal, - fiatTransactionTotal, - hexMaximumTransactionFee, - } = transactionFeeSelector(state, transaction); - const tokens = getTokens(state); - const collectibles = getCollectibles(state); - - const transactionData = getTransactionData(data); - const toAddress = getTokenAddressParam(transactionData); - const tokenAmountOrTokenId = getTokenValueParam(transactionData); - const ethTransactionTotalMaxAmount = Number( - hexWEIToDecETH(hexMaximumTransactionFee), - ).toFixed(6); - - const currentToken = tokens?.find(({ address }) => - isEqualCaseInsensitive(tokenAddress, address), - ); - const currentCollectible = collectibles?.find( - ({ address, tokenId }) => - isEqualCaseInsensitive(tokenAddress, address) && - tokenId === tokenAmountOrTokenId, - ); - - let image, - tokenId, - collectibleName, - tokenAmount, - contractExchangeRate, - title, - subtitle; - - if (currentCollectible) { - ({ image, tokenId, name: collectibleName } = currentCollectible || {}); - - title = collectibleName; - subtitle = `#${tokenId}`; - } else if (currentToken) { - const { decimals, symbol: tokenSymbol } = currentToken || {}; - tokenAmount = - transactionData && - calcTokenAmount(tokenAmountOrTokenId, decimals).toFixed(); - contractExchangeRate = contractExchangeRateSelector(state); - title = `${tokenAmount} ${tokenSymbol}`; - } - - return { - title, - subtitle, - image, - toAddress, - tokenAddress, - tokenAmount, - currentCurrency, - conversionRate, - contractExchangeRate, - fiatTransactionTotal, - ethTransactionTotal, - ethTransactionTotalMaxAmount, - nativeCurrency, - }; -}; - -export default compose( - withRouter, - connect(mapStateToProps), -)(ConfirmTokenTransactionBase); 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.js similarity index 69% rename from ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js rename to ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js index cf2da63f8..edacfaa8a 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.js @@ -1,6 +1,7 @@ import React, { useContext, useMemo } from 'react'; import PropTypes from 'prop-types'; import BigNumber from 'bignumber.js'; +import { useSelector } from 'react-redux'; import { I18nContext } from '../../contexts/i18n'; import ConfirmTransactionBase from '../confirm-transaction-base'; import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'; @@ -10,26 +11,57 @@ import { addFiat, roundExponential, } from '../../helpers/utils/confirm-tx.util'; -import { getWeiHexFromDecimalValue } from '../../helpers/utils/conversions.util'; -import { ETH, PRIMARY } from '../../helpers/constants/common'; +import { + getWeiHexFromDecimalValue, + hexWEIToDecETH, +} from '../../helpers/utils/conversions.util'; +import { + ERC1155, + ERC20, + ERC721, + ETH, + PRIMARY, +} from '../../helpers/constants/common'; +import { + contractExchangeRateSelector, + getCurrentCurrency, +} from '../../selectors'; +import { + getConversionRate, + getNativeCurrency, +} from '../../ducks/metamask/metamask'; export default function ConfirmTokenTransactionBase({ - image, - title, - subtitle, + image = '', + assetName, toAddress, tokenAddress, tokenAmount = '0', - fiatTransactionTotal, - ethTransactionTotal, - ethTransactionTotalMaxAmount, - contractExchangeRate, - conversionRate, - currentCurrency, - nativeCurrency, + tokenSymbol, + tokenId, + assetStandard, onEdit, + ethTransactionTotal, + fiatTransactionTotal, + hexMaximumTransactionFee, }) { const t = useContext(I18nContext); + const contractExchangeRate = useSelector(contractExchangeRateSelector); + const nativeCurrency = useSelector(getNativeCurrency); + const currentCurrency = useSelector(getCurrentCurrency); + const conversionRate = useSelector(getConversionRate); + + const ethTransactionTotalMaxAmount = Number( + hexWEIToDecETH(hexMaximumTransactionFee), + ); + + let title, subtitle; + if (assetStandard === ERC721 || assetStandard === ERC1155) { + title = assetName; + subtitle = `#${tokenId}`; + } else if (assetStandard === ERC20) { + title = `${tokenAmount} ${tokenSymbol}`; + } const hexWeiValue = useMemo(() => { if (tokenAmount === '0' || !contractExchangeRate) { @@ -93,7 +125,7 @@ export default function ConfirmTokenTransactionBase({ toAddress={toAddress} image={image} onEdit={onEdit} - identiconAddress={tokenAddress} + tokenAddress={tokenAddress} title={title} subtitleComponent={subtitleComponent()} primaryTotalTextOverride={`${title} + ${ethTransactionTotal} ${nativeCurrency}`} @@ -105,17 +137,15 @@ export default function ConfirmTokenTransactionBase({ ConfirmTokenTransactionBase.propTypes = { image: PropTypes.string, - title: PropTypes.string, - subtitle: PropTypes.string, - tokenAddress: PropTypes.string, + assetName: PropTypes.string, toAddress: PropTypes.string, + tokenAddress: PropTypes.string, tokenAmount: PropTypes.string, - fiatTransactionTotal: PropTypes.string, - ethTransactionTotal: PropTypes.string, - contractExchangeRate: PropTypes.number, - conversionRate: PropTypes.number, - currentCurrency: PropTypes.string, + tokenSymbol: PropTypes.string, + tokenId: PropTypes.string, + assetStandard: PropTypes.string, onEdit: PropTypes.func, - nativeCurrency: PropTypes.string, - ethTransactionTotalMaxAmount: PropTypes.string, + ethTransactionTotal: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + hexMaximumTransactionFee: PropTypes.string, }; diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js index ffe86e3ee..b1d5cf9f3 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js @@ -1,6 +1,6 @@ import React from 'react'; import { store } from '../../../.storybook/preview'; -import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'; +import ConfirmTokenTransactionBase from './confirm-token-transaction-base'; export default { title: 'Pages/ConfirmTokenTransactionBase', diff --git a/ui/pages/confirm-token-transaction-base/index.js b/ui/pages/confirm-token-transaction-base/index.js index e5b6df031..9c9fb78c4 100644 --- a/ui/pages/confirm-token-transaction-base/index.js +++ b/ui/pages/confirm-token-transaction-base/index.js @@ -1,2 +1 @@ -export { default } from './confirm-token-transaction-base.container'; -export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component'; +export { default } from './confirm-token-transaction-base'; 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..adbb684c7 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 ( @@ -537,7 +541,7 @@ export default class ConfirmTransactionBase extends Component { {t('transactionDetailDappGasMoreInfo')} @@ -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..e5c325242 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -14,11 +14,7 @@ import { setDefaultHomeActiveTabName, } from '../../store/actions'; import { isBalanceSufficient, calcGasTotal } from '../send/send.utils'; -import { - isEqualCaseInsensitive, - shortenAddress, - valuesFor, -} from '../../helpers/utils/util'; +import { shortenAddress, valuesFor } from '../../helpers/utils/util'; import { getAdvancedInlineGasShown, getCustomNonceValue, @@ -55,6 +51,7 @@ import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app'; import { isLegacyTransaction } from '../../helpers/utils/transactions.util'; import { CUSTOM_GAS_ESTIMATE } from '../../../shared/constants/gas'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import ConfirmTransactionBase from './confirm-transaction-base.component'; let customNonceValue = ''; @@ -177,7 +174,7 @@ const mapStateToProps = (state, ownProps) => { } const isCollectibleTransfer = Boolean( - allCollectibleContracts?.[selectedAddress]?.[chainId].find((contract) => { + allCollectibleContracts?.[selectedAddress]?.[chainId]?.find((contract) => { return isEqualCaseInsensitive(contract.address, fullTxData.txParams.to); }), ); @@ -285,6 +282,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('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/confirm-transaction/confirm-token-transaction-switch.js b/ui/pages/confirm-transaction/confirm-token-transaction-switch.js new file mode 100644 index 000000000..ea06c0d0b --- /dev/null +++ b/ui/pages/confirm-transaction/confirm-token-transaction-switch.js @@ -0,0 +1,125 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { Switch, Route } from 'react-router-dom'; +import { + CONFIRM_APPROVE_PATH, + CONFIRM_SEND_TOKEN_PATH, + CONFIRM_TRANSACTION_ROUTE, + CONFIRM_TRANSFER_FROM_PATH, +} from '../../helpers/constants/routes'; +import { transactionFeeSelector } from '../../selectors'; +import ConfirmApprove from '../confirm-approve'; +import ConfirmSendToken from '../confirm-send-token'; +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'; +import ConfirmTransactionSwitch from '../confirm-transaction-switch'; + +import { useAssetDetails } from '../../hooks/useAssetDetails'; + +export default function ConfirmTokenTransactionSwitch({ transaction }) { + const { + txParams: { data, to: tokenAddress, from: userAddress } = {}, + } = transaction; + + const { + assetStandard, + assetName, + userBalance, + tokenSymbol, + decimals, + tokenImage, + tokenAmount, + tokenId, + toAddress, + } = useAssetDetails(tokenAddress, userAddress, data); + + const { + ethTransactionTotal, + fiatTransactionTotal, + hexTransactionTotal, + hexMaximumTransactionFee, + } = useSelector((state) => transactionFeeSelector(state, transaction)); + + return ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + ); +} + +ConfirmTokenTransactionSwitch.propTypes = { + transaction: PropTypes.shape({ + origin: PropTypes.string, + txParams: PropTypes.shape({ + data: PropTypes.string, + to: PropTypes.string, + from: PropTypes.string, + }), + }), +}; diff --git a/ui/pages/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirm-transaction/confirm-transaction.component.js index d28904544..01e876fdb 100644 --- a/ui/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/pages/confirm-transaction/confirm-transaction.component.js @@ -5,10 +5,7 @@ import Loading from '../../components/ui/loading-screen'; import ConfirmTransactionSwitch from '../confirm-transaction-switch'; import ConfirmTransactionBase from '../confirm-transaction-base'; import ConfirmSendEther from '../confirm-send-ether'; -import ConfirmSendToken from '../confirm-send-token'; import ConfirmDeployContract from '../confirm-deploy-contract'; -import ConfirmApprove from '../confirm-approve'; -import ConfirmTokenTransactionBaseContainer from '../confirm-token-transaction-base'; import ConfirmDecryptMessage from '../confirm-decrypt-message'; import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key'; @@ -16,9 +13,6 @@ import { CONFIRM_TRANSACTION_ROUTE, CONFIRM_DEPLOY_CONTRACT_PATH, CONFIRM_SEND_ETHER_PATH, - CONFIRM_SEND_TOKEN_PATH, - CONFIRM_APPROVE_PATH, - CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, DECRYPT_MESSAGE_REQUEST_PATH, @@ -31,6 +25,7 @@ import { addPollingTokenToAppState, removePollingTokenFromAppState, } from '../../store/actions'; +import ConfirmTokenTransactionSwitch from './confirm-token-transaction-switch'; import ConfTx from './conf-tx'; export default class ConfirmTransaction extends Component { @@ -49,7 +44,6 @@ export default class ConfirmTransaction extends Component { getContractMethodData: PropTypes.func, transactionId: PropTypes.string, paramsTransactionId: PropTypes.string, - getTokenParams: PropTypes.func, isTokenMethodAction: PropTypes.bool, setDefaultHomeActiveTabName: PropTypes.func, }; @@ -74,12 +68,10 @@ export default class ConfirmTransaction extends Component { sendTo, history, mostRecentOverviewPage, - transaction: { txParams: { data, to } = {} } = {}, + transaction: { txParams: { data } = {} } = {}, getContractMethodData, transactionId, paramsTransactionId, - getTokenParams, - isTokenMethodAction, } = this.props; getGasFeeEstimatesAndStartPolling().then((pollingToken) => { @@ -100,9 +92,7 @@ export default class ConfirmTransaction extends Component { } getContractMethodData(data); - if (isTokenMethodAction) { - getTokenParams(to); - } + const txId = transactionId || paramsTransactionId; if (txId) { this.props.setTransactionToConfirm(txId); @@ -154,23 +144,30 @@ export default class ConfirmTransaction extends Component { } render() { - const { transactionId, paramsTransactionId } = this.props; + const { + transactionId, + paramsTransactionId, + isTokenMethodAction, + transaction, + } = this.props; + + const validTransactionId = + transactionId && + (!paramsTransactionId || paramsTransactionId === transactionId); + + if (isTokenMethodAction && validTransactionId) { + return ; + } // Show routes when state.confirmTransaction has been set and when either the ID in the params // isn't specified or is specified and matches the ID in state.confirmTransaction in order to // support URLs of /confirm-transaction or /confirm-transaction/ - return transactionId && - (!paramsTransactionId || paramsTransactionId === transactionId) ? ( + return validTransactionId ? ( - - - { }, clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), getContractMethodData: (data) => dispatch(getContractMethodData(data)), - getTokenParams: (tokenAddress) => dispatch(getTokenParams(tokenAddress)), setDefaultHomeActiveTabName: (tabName) => dispatch(setDefaultHomeActiveTabName(tabName)), }; diff --git a/ui/pages/confirmation/confirmation.js b/ui/pages/confirmation/confirmation.js index 44768c633..8bb7e22ea 100644 --- a/ui/pages/confirmation/confirmation.js +++ b/ui/pages/confirmation/confirmation.js @@ -166,7 +166,7 @@ export default function ConfirmationPage() { setCurrentPendingConfirmation(currentPendingConfirmation - 1) } > - + )}
    )} @@ -186,7 +186,6 @@ export default function ConfirmationPage() { {templatedValues.networkDisplay ? ( diff --git a/ui/pages/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmation/templates/add-ethereum-chain.js index 2a16852dd..db7dac420 100644 --- a/ui/pages/confirmation/templates/add-ethereum-chain.js +++ b/ui/pages/confirmation/templates/add-ethereum-chain.js @@ -204,7 +204,7 @@ function getValues(pendingApproval, t, actions) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest(), + ethErrors.provider.userRejectedRequest().serialize(), ), networkDisplay: true, }; diff --git a/ui/pages/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmation/templates/switch-ethereum-chain.js index cd28f21b0..3b09fd00e 100644 --- a/ui/pages/confirmation/templates/switch-ethereum-chain.js +++ b/ui/pages/confirmation/templates/switch-ethereum-chain.js @@ -83,7 +83,7 @@ function getValues(pendingApproval, t, actions) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest(), + ethErrors.provider.userRejectedRequest().serialize(), ), networkDisplay: true, }; 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/create-account/import-account/index.scss b/ui/pages/create-account/import-account/index.scss index 7633fab7a..9015f1f35 100644 --- a/ui/pages/create-account/import-account/index.scss +++ b/ui/pages/create-account/import-account/index.scss @@ -65,11 +65,11 @@ height: 54px; width: 315px; - border: 1px solid var(--geyser); + border: 1px solid var(--color-border-default); border-radius: 4px; - background-color: var(--white); + background-color: var(--color-background-default); + color: var(--color-text-default); margin-top: 16px; - color: var(--scorpion); padding: 0 20px; } 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..f410ed305 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 @@ -42,14 +42,14 @@ export default function SeedPhraseIntro() {
    {t('seedPhraseIntroTitle')} @@ -104,7 +104,7 @@ export default function SeedPhraseIntro() { @@ -120,7 +120,7 @@ export default function SeedPhraseIntro() { diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 004312a85..730fd7de9 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -277,14 +277,14 @@ export default class Home extends PureComponent { descriptionText={ <> {t('somethingWentWrong')} @@ -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')} , + , + , + ])} + + + {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/create-password/create-password.js b/ui/pages/onboarding-flow/create-password/create-password.js index fcdbca69a..e91e31004 100644 --- a/ui/pages/onboarding-flow/create-password/create-password.js +++ b/ui/pages/onboarding-flow/create-password/create-password.js @@ -1,6 +1,8 @@ import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; +import zxcvbn from 'zxcvbn'; +import { useSelector } from 'react-redux'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { useI18nContext } from '../../../hooks/useI18nContext'; import Button from '../../../components/ui/button'; @@ -25,6 +27,9 @@ import { TwoStepProgressBar, twoStepStages, } from '../../../components/app/step-progress-bar'; +import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; +import { getFirstTimeFlowType } from '../../../selectors'; +import { FIRST_TIME_FLOW_TYPES } from '../../../helpers/constants/onboarding'; export default function CreatePassword({ createNewAccount, @@ -35,10 +40,13 @@ export default function CreatePassword({ const [confirmPassword, setConfirmPassword] = useState(''); const [password, setPassword] = useState(''); const [passwordError, setPasswordError] = useState(''); + const [passwordStrength, setPasswordStrength] = useState(''); + const [passwordStrengthText, setPasswordStrengthText] = useState(''); const [confirmPasswordError, setConfirmPasswordError] = useState(''); const [termsChecked, setTermsChecked] = useState(false); const [showPassword, setShowPassword] = useState(false); const history = useHistory(); + const firstTimeFlowType = useSelector(getFirstTimeFlowType); const submitPasswordEvent = useNewMetricEvent({ event: 'Submit Password', @@ -57,19 +65,51 @@ export default function CreatePassword({ return !passwordError && !confirmPasswordError; }, [password, confirmPassword, passwordError, confirmPasswordError]); - const handlePasswordChange = (passwordInput) => { - let error = ''; - let confirmError = ''; - if (passwordInput && passwordInput.length < 8) { - error = t('passwordNotLongEnough'); + const getPasswordStrengthLabel = (score, translation) => { + if (score >= 4) { + return { + className: 'create-password__strong', + text: translation('strong'), + description: '', + }; + } else if (score === 3) { + return { + className: 'create-password__average', + text: translation('average'), + description: t('passwordStrengthDescription'), + }; } + return { + className: 'create-password__weak', + text: translation('weak'), + description: t('passwordStrengthDescription'), + }; + }; + + const handlePasswordChange = (passwordInput) => { + let confirmError = ''; + const passwordEvaluation = zxcvbn(passwordInput); + const passwordStrengthLabel = getPasswordStrengthLabel( + passwordEvaluation.score, + t, + ); + const passwordStrengthDescription = passwordStrengthLabel.description; + const passwordStrengthInput = t('passwordStrength', [ + + {passwordStrengthLabel.text} + , + ]); if (confirmPassword && passwordInput !== confirmPassword) { confirmError = t('passwordsDontMatch'); } setPassword(passwordInput); - setPasswordError(error); + setPasswordStrength(passwordStrengthInput); + setPasswordStrengthText(passwordStrengthDescription); setConfirmPasswordError(confirmError); }; @@ -90,7 +130,10 @@ export default function CreatePassword({ return; } // If secretRecoveryPhrase is defined we are in import wallet flow - if (secretRecoveryPhrase) { + if ( + secretRecoveryPhrase && + firstTimeFlowType === FIRST_TIME_FLOW_TYPES.IMPORT + ) { await importWithRecoveryPhrase(password, secretRecoveryPhrase); history.push(ONBOARDING_COMPLETION_ROUTE); } else { @@ -109,7 +152,8 @@ export default function CreatePassword({ return (
    - {secretRecoveryPhrase ? ( + {secretRecoveryPhrase && + firstTimeFlowType === FIRST_TIME_FLOW_TYPES.IMPORT ? ( ) : ( @@ -133,7 +177,8 @@ export default function CreatePassword({ e.stopPropagation()} key="create-password__link-text" - href="https://metamask.io/terms.html" + href={ZENDESK_URLS.PASSWORD_ARTICLE} target="_blank" rel="noopener noreferrer" > - {t('learnMore')} + {t('learnMoreUpperCase')} , ])} @@ -194,7 +239,8 @@ export default function CreatePassword({ diff --git a/ui/pages/onboarding-flow/create-password/index.scss b/ui/pages/onboarding-flow/create-password/index.scss index cc12200ae..0979fde43 100644 --- a/ui/pages/onboarding-flow/create-password/index.scss +++ b/ui/pages/onboarding-flow/create-password/index.scss @@ -1,4 +1,16 @@ .create-password { + &__weak { + color: var(--error-1); + } + + &__average { + color: var(--secondary-3);; + } + + &__strong { + color: var(--success-3); + } + &__wrapper { display: flex; justify-content: center; 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/metametrics/metametrics.js b/ui/pages/onboarding-flow/metametrics/metametrics.js index 38672c431..2577b66f0 100644 --- a/ui/pages/onboarding-flow/metametrics/metametrics.js +++ b/ui/pages/onboarding-flow/metametrics/metametrics.js @@ -123,7 +123,7 @@ export default function OnboardingMetametrics() { ,
    +
    +
    + + +
    ); 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/permissions-connect/choose-account/choose-account.js b/ui/pages/permissions-connect/choose-account/choose-account.js index 161f98532..de9da8316 100644 --- a/ui/pages/permissions-connect/choose-account/choose-account.js +++ b/ui/pages/permissions-connect/choose-account/choose-account.js @@ -48,29 +48,31 @@ const ChooseAccount = ({ }; return ( -
    - 0 - ? t('selectAccounts') - : t('connectAccountOrCreate') - } - siteOrigin={targetSubjectMetadata?.origin} - /> - + <> +
    + 0 + ? t('selectAccounts') + : t('connectAccountOrCreate') + } + siteOrigin={targetSubjectMetadata?.origin} + /> + +
    @@ -89,7 +91,7 @@ const ChooseAccount = ({
    -
    + ); }; diff --git a/ui/pages/permissions-connect/choose-account/index.scss b/ui/pages/permissions-connect/choose-account/index.scss index 910770dfd..005d126eb 100644 --- a/ui/pages/permissions-connect/choose-account/index.scss +++ b/ui/pages/permissions-connect/choose-account/index.scss @@ -1,16 +1,4 @@ .permissions-connect-choose-account { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - margin-left: auto; - margin-right: auto; - height: 100%; - - @media screen and (min-width: $break-large) { - width: 426px; - } - &__title { @include H4; } @@ -19,6 +7,16 @@ color: var(--Red-400); } + &__content { + display: flex; + overflow-y: auto; + flex-direction: column; + width: 100%; + padding-left: 24px; + padding-right: 24px; + margin-top: 2px; + } + &__footer-container { width: 100%; flex: 1; @@ -39,7 +37,7 @@ width: 100%; padding-top: 16px; padding-bottom: 16px; - margin-top: 8px; + margin-top: 12px; border-top: 1px solid #d6d9dc; @media screen and (min-width: $break-large) { diff --git a/ui/pages/permissions-connect/flask/snap-install/index.scss b/ui/pages/permissions-connect/flask/snap-install/index.scss index b15fe2aa6..711e900fc 100644 --- a/ui/pages/permissions-connect/flask/snap-install/index.scss +++ b/ui/pages/permissions-connect/flask/snap-install/index.scss @@ -28,5 +28,6 @@ .page-container__footer { width: 100%; + margin-top: 12px; } } diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 2ac1b40fa..7f5565d9a 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -69,6 +69,7 @@ import { getEnvironmentType } from '../../../app/scripts/lib/util'; import ConfirmationPage from '../confirmation'; import OnboardingFlow from '../onboarding-flow/onboarding-flow'; import QRHardwarePopover from '../../components/app/qr-hardware-popover'; +import { SEND_STAGES } from '../../ducks/send'; export default class Routes extends Component { static propTypes = { @@ -94,6 +95,8 @@ export default class Routes extends Component { prepareToLeaveSwaps: PropTypes.func, browserEnvironmentOs: PropTypes.string, browserEnvironmentBrowser: PropTypes.string, + theme: PropTypes.string, + sendStage: PropTypes.string, }; static contextTypes = { @@ -101,12 +104,22 @@ export default class Routes extends Component { metricsEvent: PropTypes.func, }; + componentDidUpdate(prevProps) { + if (process.env.DARK_MODE_V1) { + const { theme } = this.props; + if (theme !== prevProps.theme) { + document.documentElement.setAttribute('data-theme', theme); + } + } + } + UNSAFE_componentWillMount() { const { currentCurrency, pageChanged, setCurrentCurrencyToUSD, history, + theme, } = this.props; if (!currentCurrency) { setCurrentCurrencyToUSD(); @@ -117,6 +130,9 @@ export default class Routes extends Component { pageChanged(locationObj.pathname); } }); + if (process.env.DARK_MODE_V1 && theme) { + document.documentElement.setAttribute('data-theme', theme); + } } renderRoutes() { @@ -228,6 +244,10 @@ export default class Routes extends Component { ); } + onEditTransactionPage() { + return this.props.sendStage === SEND_STAGES.EDIT; + } + onSwapsPage() { const { location } = this.props; return Boolean( @@ -313,18 +333,19 @@ export default class Routes extends Component { isMouseUser, browserEnvironmentOs: os, browserEnvironmentBrowser: browser, + theme, } = this.props; const loadMessage = loadingMessage || isNetworkLoading ? this.getConnectingLabel(loadingMessage) : null; - return (
    setMouseUserState(true)} @@ -344,6 +365,7 @@ export default class Routes extends Component { onClick={this.onAppHeaderClick} disabled={ this.onConfirmPage() || + this.onEditTransactionPage() || (this.onSwapsPage() && !this.onSwapsBuildQuotePage()) } /> diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index e8f6ba6ad..4be9745ac 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -5,6 +5,7 @@ import { getNetworkIdentifier, getPreferences, isNetworkLoading, + getTheme, } from '../../selectors'; import { lockMetamask, @@ -14,6 +15,7 @@ import { } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; +import { getSendStage } from '../../ducks/send'; import Routes from './routes.component'; function mapStateToProps(state) { @@ -36,6 +38,8 @@ function mapStateToProps(state) { browserEnvironmentContainter: state.metamask.browserEnvironment?.browser, providerId: getNetworkIdentifier(state), providerType: state.metamask.provider?.type, + theme: getTheme(state), + sendStage: getSendStage(state), }; } diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.component.js b/ui/pages/send/send-content/add-recipient/add-recipient.component.js index 322dca677..100d2877f 100644 --- a/ui/pages/send/send-content/add-recipient/add-recipient.component.js +++ b/ui/pages/send/send-content/add-recipient/add-recipient.component.js @@ -7,6 +7,7 @@ import ContactList from '../../../../components/app/contact-list'; import RecipientGroup from '../../../../components/app/contact-list/recipient-group/recipient-group.component'; import { ellipsify } from '../../send.utils'; import Button from '../../../../components/ui/button'; +import IconCaretLeft from '../../../../components/ui/icon/icon-caret-left'; import Confusable from '../../../../components/ui/confusable'; export default class AddRecipient extends Component { @@ -172,7 +173,7 @@ export default class AddRecipient extends Component { className="send__select-recipient-wrapper__list__link" onClick={useContactListForRecipientSearch} > -
    + {t('backToAll')} {story()}], argTypes: { - recipient: { type: 'text', defaultValue: recipient }, - contacts: { type: 'object', defaultValue: [addressBook] }, - nonContacts: { type: 'object', defaultValue: [addressBook] }, - ownedAccounts: { type: 'object', defaultValue: [addressBook] }, - addressBook: { type: 'object', defaultValue: [addressBook] }, + userInput: { + control: 'text', + }, + ownedAccounts: { + control: 'array', + }, + addressBook: { + control: 'array', + }, + updateRecipient: { + action: 'updateRecipient', + }, + ensResolution: { + control: 'text', + }, + ensError: { + control: 'text', + }, + ensWarning: { + control: 'text', + }, + addressBookEntryName: { + control: 'text', + }, + contacts: { + control: 'array', + }, + nonContacts: { + control: 'array', + }, + useMyAccountsForRecipientSearch: { + action: 'useMyAccountsForRecipientSearch', + }, + useContactListForRecipientSearch: { + action: 'useContactListForRecipientSearch', + }, + isUsingMyAccountsForRecipientSearch: { + control: 'bool', + }, + recipient: { + control: 'object', + }, + }, + args: { + recipient, + contacts: [addressBook], + nonContacts: [addressBook], + ownedAccounts: [addressBook], + addressBook: [addressBook], }, }; diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js index 3f28f967c..34b77eff2 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -7,7 +7,7 @@ import TokenListDisplay from '../../../../components/app/token-list-display'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'; import { ERC20, ERC721, PRIMARY } from '../../../../helpers/constants/common'; import { ASSET_TYPES } from '../../../../ducks/send'; -import { isEqualCaseInsensitive } from '../../../../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; export default class SendAssetRow extends Component { static propTypes = { diff --git a/ui/pages/send/send.scss b/ui/pages/send/send.scss index 10a02f96f..bfc57a488 100644 --- a/ui/pages/send/send.scss +++ b/ui/pages/send/send.scss @@ -68,10 +68,7 @@ @extend %bg-contain; display: block; - background-image: url('/images/caret-left.svg'); - width: 18px; - height: 18px; - margin-right: 0.5rem; + margin-right: 8px; [dir='rtl'] & { transform: rotate(180deg); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index c24ddb868..60bae67da 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 (
    @@ -156,7 +172,7 @@ export default class AdvancedTab extends PureComponent {
    -
    +
    -
    +

    -
    +
    -
    +
    -
    +
    ) } @@ -383,16 +404,6 @@ export default function Swap() { ? 'swaps__error-message' : 'actionable-message--left-aligned actionable-message--warning swaps__error-message' } - primaryAction={ - isStxNotEnoughFundsError - ? null - : { - label: t('dismiss'), - onClick: () => - dispatch(dismissCurrentSmartTransactionsErrorMessage()), - } - } - withRightButton /> )} diff --git a/ui/pages/swaps/index.scss b/ui/pages/swaps/index.scss index fb428a52d..e4d837110 100644 --- a/ui/pages/swaps/index.scss +++ b/ui/pages/swaps/index.scss @@ -120,4 +120,28 @@ padding-left: 24px; flex: 1; } + + &__notification-close-button { + background-color: transparent; + position: absolute; + right: 0; + top: 2px; + + &::after { + position: absolute; + content: '\00D7'; + font-size: 29px; + font-weight: 200; + color: var(--color-text-default); + background-color: transparent; + top: 0; + right: 12px; + cursor: pointer; + } + } + + &__notification-title { + font-weight: bold; + margin-right: 14px; + } } diff --git a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js index 32394b934..23456af32 100644 --- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js +++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js @@ -11,6 +11,7 @@ import { getQuotesFetchStartTime, getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getCurrentSmartTransactionsEnabled, } from '../../../ducks/swaps/swaps'; import { isHardwareWallet, @@ -41,6 +42,9 @@ export default function LoadingSwapsQuotes({ getSmartTransactionsOptInStatus, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); + const currentSmartTransactionsEnabled = useSelector( + getCurrentSmartTransactionsEnabled, + ); const quotesRequestCancelledEventConfig = { event: 'Quotes Request Cancelled', category: 'swaps', @@ -55,6 +59,7 @@ export default function LoadingSwapsQuotes({ is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, + current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }, }; diff --git a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js index 701ae428b..2371e6091 100644 --- a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js +++ b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js @@ -142,7 +142,7 @@ export default function ItemList({ ) : null}
    {result.notImported && ( - )} diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.js index d542f2299..597709bd5 100644 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.js +++ b/ui/pages/swaps/slippage-buttons/slippage-buttons.js @@ -14,7 +14,7 @@ import { ALIGN_ITEMS, DISPLAY, } from '../../../helpers/constants/design-system'; -import { smartTransactionsErrorMessages } from '../swaps.util'; +import { getTranslatedStxErrorMessage } from '../swaps.util'; export default function SlippageButtons({ onSelect, @@ -166,11 +166,15 @@ export default function SlippageButtons({ > { - setCustomValue(event.target.value); - onSelect(Number(event.target.value)); + const { value } = event.target; + const isValueNumeric = !isNaN(Number(value)); + if (isValueNumeric) { + setCustomValue(value); + onSelect(Number(value)); + } }} - type="number" - step="0.1" + type="text" + maxLength="4" ref={setInputRef} onBlur={() => { setEnteringCustomValue(false); @@ -204,8 +208,9 @@ export default function SlippageButtons({ {currentSmartTransactionsError ? ( ) : ( @@ -215,7 +220,7 @@ export default function SlippageButtons({ { - setSmartTransactionsOptInStatus(!value); + setSmartTransactionsOptInStatus(!value, value); }} offLabel={t('off')} onLabel={t('on')} diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js index f14fdbb37..232c00639 100644 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js +++ b/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js @@ -45,6 +45,6 @@ describe('SlippageButtons', () => { expect( document.querySelector('.slippage-buttons__button-group'), ).toMatchSnapshot(); - expect(getByText('Smart transaction')).toBeInTheDocument(); + expect(getByText('Smart Transaction')).toBeInTheDocument(); }); }); diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index 8087020b2..f76d616f5 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -184,6 +184,8 @@ export default function SmartTransactionStatus() { headerText = t('stxPendingFinalizing'); } else if (timeLeftForPendingStxInSec < 150) { headerText = t('stxPendingPrivatelySubmitting'); + } else if (cancelSwapLinkClicked) { + headerText = t('stxTryingToCancel'); } } if (smartTransactionStatus === SMART_TRANSACTION_STATUSES.SUCCESS) { @@ -192,7 +194,11 @@ export default function SmartTransactionStatus() { description = t('stxSuccessDescription', [destinationTokenInfo.symbol]); } icon = ; - } else if (smartTransactionStatus === 'cancelled_user_cancelled') { + } else if ( + smartTransactionStatus === 'cancelled_user_cancelled' || + latestSmartTransaction?.statusMetadata?.minedTx === + SMART_TRANSACTION_STATUSES.CANCELLED + ) { headerText = t('stxUserCancelled'); description = t('stxUserCancelledDescription'); icon = ; @@ -263,11 +269,11 @@ export default function SmartTransactionStatus() { justifyContent={JUSTIFY_CONTENT.CENTER} alignItems={ALIGN_ITEMS.CENTER} > - + {`${fetchParams?.value && Number(fetchParams.value).toFixed(5)} `} {`~${destinationValue && Number(destinationValue).toFixed(5)} `} {`${t('swapCompleteIn')} `} )} @@ -366,7 +372,7 @@ export default function SmartTransactionStatus() { {description} @@ -379,7 +385,7 @@ export default function SmartTransactionStatus() { {subDescription} diff --git a/ui/pages/swaps/swaps.util.js b/ui/pages/swaps/swaps.util.js index 14b7e4b77..9c4f6b52d 100644 --- a/ui/pages/swaps/swaps.util.js +++ b/ui/pages/swaps/swaps.util.js @@ -55,6 +55,7 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const CACHE_REFRESH_FIVE_MINUTES = 300000; +const USD_CURRENCY_CODE = 'usd'; const clientIdHeader = { 'X-Client-Id': SWAPS_CLIENT_ID }; @@ -514,12 +515,25 @@ export const getFeeForSmartTransaction = ({ conversionRate, numberOfDecimals: 2, }); + let feeInUsd; + if (currentCurrency === USD_CURRENCY_CODE) { + feeInUsd = rawNetworkFees; + } else { + feeInUsd = getValueFromWeiHex({ + value: feeInWeiHex, + toCurrency: USD_CURRENCY_CODE, + conversionRate, + numberOfDecimals: 2, + }); + } const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency); const chainCurrencySymbolToUse = nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol; return { + feeInUsd, feeInFiat: formattedNetworkFee, feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`, + rawEthFee: ethFee, }; }; @@ -564,11 +578,24 @@ export function getRenderableNetworkFeesForQuote({ }); const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency); + let feeInUsd; + if (currentCurrency === USD_CURRENCY_CODE) { + feeInUsd = rawNetworkFees; + } else { + feeInUsd = getValueFromWeiHex({ + value: totalWeiCost, + toCurrency: USD_CURRENCY_CODE, + conversionRate, + numberOfDecimals: 2, + }); + } + const chainCurrencySymbolToUse = nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol; return { rawNetworkFees, + feeInUsd, rawEthFee: ethFee, feeInFiat: formattedNetworkFee, feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`, @@ -903,18 +930,22 @@ export const showRemainingTimeInMinAndSec = (remainingTimeInSec) => { return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; -export const stxErrorTypes = ['unavailable', 'not_enough_funds']; - -const smartTransactionsErrorMap = { - unavailable: 'Smart Transactions are temporarily unavailable.', - not_enough_funds: 'Not enough funds for a smart transaction.', +export const stxErrorTypes = { + UNAVAILABLE: 'unavailable', + NOT_ENOUGH_FUNDS: 'not_enough_funds', + REGULAR_TX_PENDING: 'regular_tx_pending', }; -export const smartTransactionsErrorMessages = (errorType) => { - return ( - smartTransactionsErrorMap[errorType] || - smartTransactionsErrorMap.unavailable - ); +export const getTranslatedStxErrorMessage = (errorType, t) => { + switch (errorType) { + case stxErrorTypes.UNAVAILABLE: + case stxErrorTypes.REGULAR_TX_PENDING: + return t('stxErrorUnavailable'); + case stxErrorTypes.NOT_ENOUGH_FUNDS: + return t('stxErrorNotEnoughFunds'); + default: + return t('stxErrorUnavailable'); + } }; export const parseSmartTransactionsError = (errorMessage) => { diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 622985580..98a9bcd4c 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -59,10 +59,7 @@ import { } from '../../../selectors'; import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; -import { - toPrecisionWithoutTrailingZeros, - isEqualCaseInsensitive, -} from '../../../helpers/utils/util'; +import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util'; import { safeRefetchQuotes, @@ -115,6 +112,7 @@ import CountdownTimer from '../countdown-timer'; import SwapsFooter from '../swaps-footer'; import PulseLoader from '../../../components/ui/pulse-loader'; // TODO: Replace this with a different loading component. import Box from '../../../components/ui/box'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; import ViewQuotePriceDifference from './view-quote-price-difference'; let intervalId; @@ -207,40 +205,6 @@ export default function ViewQuote() { const swapsRefreshRates = useSelector(getSwapsRefreshStates); const unsignedTransaction = usedQuote.trade; - useEffect(() => { - if (currentSmartTransactionsEnabled && smartTransactionsOptInStatus) { - const unsignedTx = { - from: unsignedTransaction.from, - to: unsignedTransaction.to, - value: unsignedTransaction.value, - data: unsignedTransaction.data, - gas: unsignedTransaction.gas, - chainId, - }; - intervalId = setInterval(() => { - dispatch( - estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams), - ); - }, swapsRefreshRates.stxGetTransactionsRefreshTime); - dispatch(estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams)); - } else if (intervalId) { - clearInterval(intervalId); - } - return () => clearInterval(intervalId); - // eslint-disable-next-line - }, [ - dispatch, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - unsignedTransaction.data, - unsignedTransaction.from, - unsignedTransaction.value, - unsignedTransaction.gas, - unsignedTransaction.to, - chainId, - swapsRefreshRates.stxGetTransactionsRefreshTime, - ]); - let gasFeeInputs; if (networkAndAccountSupports1559) { // For Swaps we want to get 'high' estimations by default. @@ -254,6 +218,17 @@ export default function ViewQuote() { const fetchParamsSourceToken = fetchParams?.sourceToken; + const additionalTrackingParams = { + reg_tx_fee_in_usd: undefined, + reg_tx_fee_in_eth: undefined, + reg_tx_max_fee_in_usd: undefined, + reg_tx_max_fee_in_eth: undefined, + stx_fee_in_usd: undefined, + stx_fee_in_eth: undefined, + stx_max_fee_in_usd: undefined, + stx_max_fee_in_eth: undefined, + }; + const usedGasLimit = usedQuote?.gasEstimateWithRefund || `0x${decimalToHex(usedQuote?.averageGas || 0)}`; @@ -268,7 +243,7 @@ export default function ViewQuote() { const nonCustomMaxGasLimit = usedQuote?.gasEstimate ? usedGasLimitWithMultiplier : `0x${decimalToHex(usedQuote?.maxGas || 0)}`; - let maxGasLimit = customMaxGas || nonCustomMaxGasLimit; + const maxGasLimit = customMaxGas || nonCustomMaxGasLimit; let maxFeePerGas; let maxPriorityFeePerGas; @@ -291,17 +266,6 @@ export default function ViewQuote() { ); } - // Smart Transactions gas fees. - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionEstimatedGas?.txData - ) { - maxGasLimit = `0x${decimalToHex( - smartTransactionEstimatedGas?.txData.gasLimit || 0, - )}`; - } - const gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); const { tokensWithBalances } = useTokenTracker(swapsTokens, true); @@ -376,7 +340,12 @@ export default function ViewQuote() { sourceTokenIconUrl, } = renderableDataForUsedQuote; - let { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote({ + let { + feeInFiat, + feeInEth, + rawEthFee, + feeInUsd, + } = getRenderableNetworkFeesForQuote({ tradeGas: usedGasLimit, approveGas, gasPrice: networkAndAccountSupports1559 @@ -390,6 +359,8 @@ export default function ViewQuote() { chainId, nativeCurrencySymbol, }); + additionalTrackingParams.reg_tx_fee_in_usd = Number(feeInUsd); + additionalTrackingParams.reg_tx_fee_in_eth = Number(rawEthFee); const renderableMaxFees = getRenderableNetworkFeesForQuote({ tradeGas: maxGasLimit, @@ -403,8 +374,15 @@ export default function ViewQuote() { chainId, nativeCurrencySymbol, }); - let { feeInFiat: maxFeeInFiat, feeInEth: maxFeeInEth } = renderableMaxFees; + let { + feeInFiat: maxFeeInFiat, + feeInEth: maxFeeInEth, + rawEthFee: maxRawEthFee, + feeInUsd: maxFeeInUsd, + } = renderableMaxFees; const { nonGasFee } = renderableMaxFees; + additionalTrackingParams.reg_tx_max_fee_in_usd = Number(maxFeeInUsd); + additionalTrackingParams.reg_tx_max_fee_in_eth = Number(maxRawEthFee); if ( currentSmartTransactionsEnabled && @@ -415,16 +393,22 @@ export default function ViewQuote() { smartTransactionEstimatedGas.txData.feeEstimate + (smartTransactionEstimatedGas.approvalTxData?.feeEstimate || 0); const stxMaxFeeInWeiDec = stxEstimatedFeeInWeiDec * 2; - ({ feeInFiat, feeInEth } = getFeeForSmartTransaction({ + ({ feeInFiat, feeInEth, rawEthFee, feeInUsd } = getFeeForSmartTransaction({ chainId, currentCurrency, conversionRate, nativeCurrencySymbol, feeInWeiDec: stxEstimatedFeeInWeiDec, })); + additionalTrackingParams.stx_fee_in_usd = Number(feeInUsd); + additionalTrackingParams.stx_fee_in_eth = Number(rawEthFee); + additionalTrackingParams.estimated_gas = + smartTransactionEstimatedGas.txData.gasLimit; ({ feeInFiat: maxFeeInFiat, feeInEth: maxFeeInEth, + rawEthFee: maxRawEthFee, + feeInUsd: maxFeeInUsd, } = getFeeForSmartTransaction({ chainId, currentCurrency, @@ -432,6 +416,8 @@ export default function ViewQuote() { nativeCurrencySymbol, feeInWeiDec: stxMaxFeeInWeiDec, })); + additionalTrackingParams.stx_max_fee_in_usd = Number(maxFeeInUsd); + additionalTrackingParams.stx_max_fee_in_eth = Number(maxRawEthFee); } const tokenCost = new BigNumber(usedQuote.sourceAmount); @@ -522,7 +508,7 @@ export default function ViewQuote() { available_quotes: numberOfQuotes, is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, - stx_enabled: currentSmartTransactionsEnabled, + stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }; @@ -784,6 +770,55 @@ export default function ViewQuote() { const isShowingWarning = showInsufficientWarning || shouldShowPriceDifferenceWarning; + const isSwapButtonDisabled = + submitClicked || + balanceError || + tokenBalanceUnavailable || + disableSubmissionDueToPriceWarning || + (networkAndAccountSupports1559 && baseAndPriorityFeePerGas === undefined) || + (!networkAndAccountSupports1559 && + (gasPrice === null || gasPrice === undefined)) || + (currentSmartTransactionsEnabled && currentSmartTransactionsError); + + useEffect(() => { + if ( + currentSmartTransactionsEnabled && + smartTransactionsOptInStatus && + !isSwapButtonDisabled + ) { + const unsignedTx = { + from: unsignedTransaction.from, + to: unsignedTransaction.to, + value: unsignedTransaction.value, + data: unsignedTransaction.data, + gas: unsignedTransaction.gas, + chainId, + }; + intervalId = setInterval(() => { + dispatch( + estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams), + ); + }, swapsRefreshRates.stxGetTransactionsRefreshTime); + dispatch(estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams)); + } else if (intervalId) { + clearInterval(intervalId); + } + return () => clearInterval(intervalId); + // eslint-disable-next-line + }, [ + dispatch, + currentSmartTransactionsEnabled, + smartTransactionsOptInStatus, + unsignedTransaction.data, + unsignedTransaction.from, + unsignedTransaction.value, + unsignedTransaction.gas, + unsignedTransaction.to, + chainId, + swapsRefreshRates.stxGetTransactionsRefreshTime, + isSwapButtonDisabled, + ]); + const onCloseEditGasPopover = () => { setShowEditGasPopover(false); }; @@ -969,10 +1004,17 @@ export default function ViewQuote() { unsignedTransaction, metaMetricsEvent, history, + additionalTrackingParams, }), ); } else { - dispatch(signAndSendTransactions(history, metaMetricsEvent)); + dispatch( + signAndSendTransactions( + history, + metaMetricsEvent, + additionalTrackingParams, + ), + ); } } else if (destinationToken.symbol === defaultSwapsToken.symbol) { history.push(DEFAULT_ROUTE); @@ -988,17 +1030,7 @@ export default function ViewQuote() { : t('swap') } hideCancel - disabled={ - submitClicked || - balanceError || - tokenBalanceUnavailable || - disableSubmissionDueToPriceWarning || - (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || - (!networkAndAccountSupports1559 && - (gasPrice === null || gasPrice === undefined)) || - (currentSmartTransactionsEnabled && currentSmartTransactionsError) - } + disabled={isSwapButtonDisabled} className={isShowingWarning && 'view-quote__thin-swaps-footer'} showTopBorder /> diff --git a/ui/pages/token-details/token-details-page.js b/ui/pages/token-details/token-details-page.js index a26ada620..592a93691 100644 --- a/ui/pages/token-details/token-details-page.js +++ b/ui/pages/token-details/token-details-page.js @@ -4,7 +4,6 @@ import { Redirect, useHistory, useParams } from 'react-router-dom'; import { getTokens } from '../../ducks/metamask/metamask'; import { getUseTokenDetection, getTokenList } from '../../selectors'; import { useCopyToClipboard } from '../../hooks/useCopyToClipboard'; -import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import Identicon from '../../components/ui/identicon'; import { I18nContext } from '../../contexts/i18n'; import { useTokenTracker } from '../../hooks/useTokenTracker'; @@ -25,6 +24,7 @@ import { TEXT_ALIGN, OVERFLOW_WRAP, } from '../../helpers/constants/design-system'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; export default function TokenDetailsPage() { const dispatch = useDispatch(); @@ -74,7 +74,7 @@ export default function TokenDetailsPage() { fontWeight={FONT_WEIGHT.BOLD} margin={[4, 0, 0, 0]} variant={TYPOGRAPHY.H6} - color={COLORS.BLACK} + color={COLORS.TEXT_DEFAULT} className="token-details__title" > {t('tokenDetails')} @@ -90,10 +90,10 @@ export default function TokenDetailsPage() { fontWeight={FONT_WEIGHT.BOLD} margin={[0, 5, 0, 0]} variant={TYPOGRAPHY.H4} - color={COLORS.BLACK} + color={COLORS.TEXT_DEFAULT} className="token-details__token-value" > - {tokenBalance} + {tokenBalance || ''} {tokenCurrencyBalance || ''} {t('tokenContractAddress')} @@ -122,7 +122,7 @@ export default function TokenDetailsPage() { @@ -147,7 +147,7 @@ export default function TokenDetailsPage() { {t('tokenDecimalTitle')} @@ -155,14 +155,14 @@ export default function TokenDetailsPage() { {token.decimals} {t('network')} @@ -170,7 +170,7 @@ export default function TokenDetailsPage() { {networkType === NETWORK_TYPE_RPC ? networkNickname ?? t('privateNetwork') @@ -185,7 +185,7 @@ export default function TokenDetailsPage() { ); }} > - + {t('hideToken')} diff --git a/ui/pages/token-details/token-details-page.test.js b/ui/pages/token-details/token-details-page.test.js index e5385098f..a4eb5bc74 100644 --- a/ui/pages/token-details/token-details-page.test.js +++ b/ui/pages/token-details/token-details-page.test.js @@ -3,7 +3,7 @@ import configureMockStore from 'redux-mock-store'; import { fireEvent } from '@testing-library/react'; import { renderWithProvider } from '../../../test/lib/render-helpers'; import Identicon from '../../components/ui/identicon/identicon.component'; -import { isEqualCaseInsensitive } from '../../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import TokenDetailsPage from './token-details-page'; const testTokenAddress = '0xaD6D458402F60fD3Bd25163575031ACDce07538A'; diff --git a/ui/pages/unlock-page/README.mdx b/ui/pages/unlock-page/README.mdx index 292d21265..bcd2e1360 100644 --- a/ui/pages/unlock-page/README.mdx +++ b/ui/pages/unlock-page/README.mdx @@ -21,7 +21,7 @@ ArgsTable doesn't work with SectionShape | -------------------------- | -------------------------------------------------------------------------- | | `history` | History router for redirect after action `object` | | `isUnlocked` | If isUnlocked is true will redirect to most recent route in history `bool` | -| `onRestore` | onClick handler for "import using Secret Recovery Phrase" link `func` | +| `onRestore` | onClick handler for "Forgot password?" link `func` | | `onSubmit` | onSumbit handler when form is submitted `func` | | `forceUpdateMetamaskState` | Force update metamask data state `func` | | `showOptInModal` | Event handler to show metametrics modal `func` | diff --git a/ui/pages/unlock-page/index.scss b/ui/pages/unlock-page/index.scss index bf83eadc8..6d816a21f 100644 --- a/ui/pages/unlock-page/index.scss +++ b/ui/pages/unlock-page/index.scss @@ -38,18 +38,11 @@ width: 100%; text-align: center; font-size: 0.75rem; - - button { - font-weight: bold; - border-radius: 100px; - } } &__link { - cursor: pointer; - background-color: unset; - color: var(--Blue-500); font-size: unset; + font-weight: bold; } &__support { diff --git a/ui/pages/unlock-page/unlock-page.component.js b/ui/pages/unlock-page/unlock-page.component.js index 50dda7c8b..b64a024ed 100644 --- a/ui/pages/unlock-page/unlock-page.component.js +++ b/ui/pages/unlock-page/unlock-page.component.js @@ -24,7 +24,7 @@ export default class UnlockPage extends Component { */ isUnlocked: PropTypes.bool, /** - * onClick handler for "import using Secret Recovery Phrase" link + * onClick handler for "Forgot password?" link */ onRestore: PropTypes.func, /** @@ -184,15 +184,14 @@ export default class UnlockPage extends Component { {this.renderSubmitButton()}
    - {t('importAccountText', [ - , - ])} +
    {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/selectors/confirm-transaction.js b/ui/selectors/confirm-transaction.js index 2f1d9d193..e3e8f6efb 100644 --- a/ui/selectors/confirm-transaction.js +++ b/ui/selectors/confirm-transaction.js @@ -25,7 +25,7 @@ import { getMaximumGasTotalInHexWei, getMinimumGasTotalInHexWei, } from '../../shared/modules/gas.utils'; -import { isEqualCaseInsensitive } from '../helpers/utils/util'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { getAveragePriceEstimateInHexWEI } from './custom-gas'; import { getCurrentChainId, deprecatedGetCurrentNetworkId } from './selectors'; import { checkNetworkAndAccountSupports1559 } from '.'; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index c287422e9..29b904e01 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -33,11 +33,7 @@ import { ALLOWED_SWAPS_CHAIN_IDS, } from '../../shared/constants/swaps'; -import { - shortenAddress, - getAccountByAddress, - isEqualCaseInsensitive, -} from '../helpers/utils/util'; +import { shortenAddress, getAccountByAddress } from '../helpers/utils/util'; import { getValueFromWeiHex, hexToDecimal, @@ -60,6 +56,7 @@ import { getLedgerWebHidConnectedStatus, getLedgerTransportStatus, } from '../ducks/app/app'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; /** * One of the only remaining valid uses of selecting the network subkey of the @@ -484,6 +481,10 @@ function getSuggestedAssetCount(state) { return suggestedAssets.length; } +export function getSuggestedAssets(state) { + return state.metamask.suggestedAssets; +} + export function getIsMainnet(state) { const chainId = getCurrentChainId(state); return chainId === MAINNET_CHAIN_ID; @@ -789,6 +790,16 @@ export function getOpenSeaEnabled(state) { return Boolean(state.metamask.openSeaEnabled); } +/** + * To get the `theme` value which determines which theme is selected + * + * @param {*} state + * @returns Boolean + */ +export function getTheme(state) { + return state.metamask.theme; +} + /** * To retrieve the tokenList produced by TokenListcontroller * 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/ui/store/actions.js b/ui/store/actions.js index 71e0bf750..d17cca23e 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -8,8 +8,6 @@ import { loadRelativeTimeFormatLocaleData, } from '../helpers/utils/i18n-helper'; import { getMethodDataAsync } from '../helpers/utils/transactions.util'; -import { getSymbolAndDecimals } from '../helpers/utils/token-util'; -import { isEqualCaseInsensitive } from '../helpers/utils/util'; import switchDirection from '../helpers/utils/switch-direction'; import { ENVIRONMENT_TYPE_NOTIFICATION, @@ -23,7 +21,6 @@ import { getMetaMaskAccounts, getPermittedAccountsForCurrentTab, getSelectedAddress, - getTokenList, } from '../selectors'; import { computeEstimatedGasLimit, resetSendState } from '../ducks/send'; import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'; @@ -35,6 +32,7 @@ import { LEDGER_USB_VENDOR_ID, } from '../../shared/constants/hardware-wallets'; import { parseSmartTransactionsError } from '../pages/swaps/swaps.util'; +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import * as actionConstants from './actionConstants'; let background = null; @@ -82,39 +80,20 @@ export function tryUnlockMetamask(password) { }; } -/** - * Adds a new account where all data is encrypted using the given password and - * where all addresses are generated from a given seed phrase. - * - * @param {string} password - The password. - * @param {string} seedPhrase - The seed phrase. - * @returns {Object} The updated state of the keyring controller. - */ -export function createNewVaultAndRestore(password, seedPhrase) { +export function createNewVaultAndRestore(password, seed) { return (dispatch) => { dispatch(showLoadingIndication()); log.debug(`background.createNewVaultAndRestore`); - - // Encode the secret recovery phrase as an array of integers so that it is - // serialized as JSON properly. - const encodedSeedPhrase = Array.from( - Buffer.from(seedPhrase, 'utf8').values(), - ); - let vault; return new Promise((resolve, reject) => { - background.createNewVaultAndRestore( - password, - encodedSeedPhrase, - (err, _vault) => { - if (err) { - reject(err); - return; - } - vault = _vault; - resolve(); - }, - ); + background.createNewVaultAndRestore(password, seed, (err, _vault) => { + if (err) { + reject(err); + return; + } + vault = _vault; + resolve(); + }); }) .then(() => dispatch(unMarkPasswordForgotten())) .then(() => { @@ -136,8 +115,8 @@ export function createNewVaultAndGetSeedPhrase(password) { try { await createNewVault(password); - const seedPhrase = await verifySeedPhrase(); - return seedPhrase; + const seedWords = await verifySeedPhrase(); + return seedWords; } catch (error) { dispatch(displayWarning(error.message)); throw new Error(error.message); @@ -153,9 +132,9 @@ export function unlockAndGetSeedPhrase(password) { try { await submitPassword(password); - const seedPhrase = await verifySeedPhrase(); + const seedWords = await verifySeedPhrase(); await forceUpdateMetamaskState(dispatch); - return seedPhrase; + return seedWords; } catch (error) { dispatch(displayWarning(error.message)); throw new Error(error.message); @@ -204,9 +183,17 @@ export function verifyPassword(password) { }); } -export async function verifySeedPhrase() { - const encodedSeedPhrase = await promisifiedBackground.verifySeedPhrase(); - return Buffer.from(encodedSeedPhrase).toString('utf8'); +export function verifySeedPhrase() { + return new Promise((resolve, reject) => { + background.verifySeedPhrase((error, seedWords) => { + if (error) { + reject(error); + return; + } + + resolve(seedWords); + }); + }); } export function requestRevealSeedWords(password) { @@ -216,11 +203,11 @@ export function requestRevealSeedWords(password) { try { await verifyPassword(password); - const seedPhrase = await verifySeedPhrase(); - return seedPhrase; + const seedWords = await verifySeedPhrase(); + return seedWords; } catch (error) { dispatch(displayWarning(error.message)); - throw error; + throw new Error(error.message); } finally { dispatch(hideLoadingIndication()); } @@ -684,6 +671,82 @@ const updateMetamaskStateFromBackground = () => { }); }; +export function updateSwapApprovalTransaction(txId, txSwapApproval) { + return async (dispatch) => { + let updatedTransaction; + try { + updatedTransaction = await promisifiedBackground.updateSwapApprovalTransaction( + txId, + txSwapApproval, + ); + } catch (error) { + dispatch(txError(error)); + dispatch(goHome()); + log.error(error.message); + throw error; + } + + return updatedTransaction; + }; +} + +export function updateEditableParams(txId, editableParams) { + return async (dispatch) => { + let updatedTransaction; + try { + updatedTransaction = await promisifiedBackground.updateEditableParams( + txId, + editableParams, + ); + } catch (error) { + dispatch(txError(error)); + dispatch(goHome()); + log.error(error.message); + throw error; + } + + return updatedTransaction; + }; +} + +export function updateTransactionGasFees(txId, txGasFees) { + return async (dispatch) => { + let updatedTransaction; + try { + updatedTransaction = await promisifiedBackground.updateTransactionGasFees( + txId, + txGasFees, + ); + } catch (error) { + dispatch(txError(error)); + dispatch(goHome()); + log.error(error.message); + throw error; + } + + return updatedTransaction; + }; +} + +export function updateSwapTransaction(txId, txSwap) { + return async (dispatch) => { + let updatedTransaction; + try { + updatedTransaction = await promisifiedBackground.updateSwapTransaction( + txId, + txSwap, + ); + } catch (error) { + dispatch(txError(error)); + dispatch(goHome()); + log.error(error.message); + throw error; + } + + return updatedTransaction; + }; +} + export function updateTransaction(txData, dontShowLoadingIndicator) { return async (dispatch) => { !dontShowLoadingIndicator && dispatch(showLoadingIndication()); @@ -1516,6 +1579,7 @@ export function rejectWatchAsset(suggestedAssetID) { dispatch(showLoadingIndication()); try { await promisifiedBackground.rejectWatchAsset(suggestedAssetID); + await forceUpdateMetamaskState(dispatch); } catch (error) { log.error(error); dispatch(displayWarning(error.message)); @@ -1532,6 +1596,7 @@ export function acceptWatchAsset(suggestedAssetID) { dispatch(showLoadingIndication()); try { await promisifiedBackground.acceptWatchAsset(suggestedAssetID); + await forceUpdateMetamaskState(dispatch); } catch (error) { log.error(error); dispatch(displayWarning(error.message)); @@ -2336,6 +2401,18 @@ export function setEIP1559V2Enabled(val) { }; } +export function setTheme(val) { + return async (dispatch) => { + dispatch(showLoadingIndication()); + log.debug(`background.setTheme`); + try { + await promisifiedBackground.setTheme(val); + } finally { + dispatch(hideLoadingIndication()); + } + }; +} + export function setIpfsGateway(val) { return (dispatch) => { dispatch(showLoadingIndication()); @@ -2830,46 +2907,6 @@ export function loadingTokenParamsFinished() { }; } -export function getTokenParams(address) { - return (dispatch, getState) => { - const tokenList = getTokenList(getState()); - const existingTokens = getState().metamask.tokens; - const { selectedAddress } = getState().metamask; - const { chainId } = getState().metamask.provider; - const existingCollectibles = getState().metamask?.allCollectibles?.[ - selectedAddress - ]?.[chainId]; - const existingToken = existingTokens.find(({ address: tokenAddress }) => - isEqualCaseInsensitive(address, tokenAddress), - ); - const existingCollectible = existingCollectibles?.find( - ({ address: collectibleAddress }) => - isEqualCaseInsensitive(address, collectibleAddress), - ); - - if (existingCollectible) { - return null; - } - - if (existingToken) { - return Promise.resolve({ - symbol: existingToken.symbol, - decimals: existingToken.decimals, - }); - } - - dispatch(loadingTokenParamsStarted()); - log.debug(`loadingTokenParams`); - - return getSymbolAndDecimals(address, tokenList).then( - ({ symbol, decimals }) => { - dispatch(addToken(address, symbol, Number(decimals))); - dispatch(loadingTokenParamsFinished()); - }, - ); - }; -} - export function setSeedPhraseBackedUp(seedPhraseBackupState) { return (dispatch) => { log.debug(`background.setSeedPhraseBackedUp`); @@ -3206,7 +3243,10 @@ export async function setWeb3ShimUsageAlertDismissed(origin) { } // Smart Transactions Controller -export async function setSmartTransactionsOptInStatus(optInState) { +export async function setSmartTransactionsOptInStatus( + optInState, + prevOptInState, +) { trackMetaMetricsEvent({ event: 'STX OptIn', category: 'swaps', @@ -3214,6 +3254,7 @@ export async function setSmartTransactionsOptInStatus(optInState) { stx_enabled: true, current_stx_enabled: true, stx_user_opt_in: optInState, + stx_prev_user_opt_in: prevOptInState, }, }); await promisifiedBackground.setSmartTransactionsOptInStatus(optInState); diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 727835ecf..0866713b4 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -111,9 +111,7 @@ describe('Actions', () => { actions._setBackgroundConnection(background); - await store.dispatch( - actions.createNewVaultAndRestore('password', 'test'), - ); + await store.dispatch(actions.createNewVaultAndRestore()); expect(createNewVaultAndRestore.callCount).toStrictEqual(1); }); @@ -136,9 +134,7 @@ describe('Actions', () => { { type: 'HIDE_LOADING_INDICATION' }, ]; - await store.dispatch( - actions.createNewVaultAndRestore('password', 'test'), - ); + await store.dispatch(actions.createNewVaultAndRestore()); expect(store.getActions()).toStrictEqual(expectedActions); }); @@ -159,7 +155,7 @@ describe('Actions', () => { ]; await expect( - store.dispatch(actions.createNewVaultAndRestore('password', 'test')), + store.dispatch(actions.createNewVaultAndRestore()), ).rejects.toThrow('error'); expect(store.getActions()).toStrictEqual(expectedActions); @@ -178,7 +174,7 @@ describe('Actions', () => { cb(), ); const verifySeedPhrase = background.verifySeedPhrase.callsFake((cb) => - cb(null, Array.from(Buffer.from('test').values())), + cb(), ); actions._setBackgroundConnection(background); diff --git a/yarn.lock b/yarn.lock index f0fdbf330..eda87c25d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1423,7 +1423,7 @@ ethers "^5.4.5" lodash "^4.17.21" -"@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.3.1", "@ethereumjs/common@^2.4.0": +"@ethereumjs/common@2.4.0", "@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.3.1", "@ethereumjs/common@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.4.0.tgz#2d67f6e6ba22246c5c89104e6b9a119fb3039766" integrity sha512-UdkhFWzWcJCZVsj1O/H8/oqj/0RVYjLc1OhPjBrQdALAkQHpCp8xXI4WLnuGTADqTdJZww0NtgwG+TRPkXt27w== @@ -1439,7 +1439,7 @@ "@ethereumjs/common" "^2.0.0" ethereumjs-util "^7.0.7" -"@ethereumjs/tx@^3.1.1", "@ethereumjs/tx@^3.2.0", "@ethereumjs/tx@^3.2.1", "@ethereumjs/tx@^3.3.0": +"@ethereumjs/tx@3.3.0", "@ethereumjs/tx@^3.2.0", "@ethereumjs/tx@^3.2.1", "@ethereumjs/tx@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.0.tgz#14ed1b7fa0f28e1cd61e3ecbdab824205f6a4378" integrity sha512-yTwEj2lVzSMgE6Hjw9Oa1DZks/nKTWM8Wn4ykDNapBPua2f4nXO3qKnni86O6lgDj5fVNRqbDsD0yy7/XNGDEA== @@ -2787,6 +2787,11 @@ web3 "^0.20.7" web3-provider-engine "^16.0.3" +"@metamask/design-tokens@^1.3.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-1.4.2.tgz#023030f3eca181b10bf89c5813a9656f4e7e2852" + integrity sha512-kS63Tx+WOUloBTz4pDDG3DcisoXwaT+06/a2KTSDI0n1t3IQPLo1FCMsijqtYWFPfAI06tWOjtaslpFTB+dsAg== + "@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" @@ -11200,16 +11205,18 @@ eth-keyring-controller@^6.2.0, eth-keyring-controller@^6.2.1: loglevel "^1.5.0" obs-store "^4.0.3" -eth-lattice-keyring@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/eth-lattice-keyring/-/eth-lattice-keyring-0.5.0.tgz#19dbbc2de31008dfd576531fa44a5d34061e61c8" - integrity sha512-+6iTQqrAqneUDfLR5aLVmYtys++a8C5N6F/9ibEtGT3YNYUE0NLZMI/DvFz/JhjpHhTjSXXtSb3yUajW6liAaQ== +eth-lattice-keyring@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/eth-lattice-keyring/-/eth-lattice-keyring-0.6.1.tgz#ebb6dfb8039d4c4c979280110aa49a7b74064850" + integrity sha512-FzP/TPIFuyo37B4dGKg6bQN0H61h9Bdejh9bxH6sUqmDB+X+a/PlydeWxZIhEjmIvD+Huuy+bcp9hXTcCcr2Ig== dependencies: - "@ethereumjs/common" "^2.4.0" - "@ethereumjs/tx" "^3.1.1" - bignumber.js "^9.0.1" + "@ethereumjs/common" "2.4.0" + "@ethereumjs/tx" "3.3.0" + bn.js "^5.2.0" ethereumjs-util "^7.0.10" - gridplus-sdk "^1.0.0" + gridplus-sdk "^1.1.6" + rlp "^3.0.0" + secp256k1 "4.0.2" eth-lib@0.2.8: version "0.2.8" @@ -12144,7 +12151,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== @@ -13584,11 +13591,13 @@ graphql-subscriptions@^1.1.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== -gridplus-sdk@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gridplus-sdk/-/gridplus-sdk-1.0.0.tgz#008a24a8ec5b50a6fdb8005723a77f5d24fc88cd" - integrity sha512-vVyLyAY7Ockkf8hv+em1KkjPwvKkLmb7mYZFY2Vtt60+qpmPut1S2/WjrZGdNGGawWAKAmpw8WKdw5MSg2UkpA== +gridplus-sdk@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/gridplus-sdk/-/gridplus-sdk-1.1.6.tgz#abec7eab81daa295806aaafda50cf3b463a53ae6" + integrity sha512-AjpnKouda18gwJpOqZRB9aY/cXKGLCj2U915hxjtlQvulRQfCtS1aNA71ZeN/rlBb77NPDZ8v1jJVerte4vKLQ== dependencies: + "@ethereumjs/common" "2.4.0" + "@ethereumjs/tx" "3.3.0" aes-js "^3.1.1" bech32 "^2.0.0" bignumber.js "^9.0.1" @@ -13601,6 +13610,7 @@ gridplus-sdk@^1.0.0: eth-eip712-util-browser "^0.0.3" hash.js "^1.1.7" js-sha3 "^0.8.0" + rlp "^3.0.0" rlp-browser "^1.0.1" secp256k1 "4.0.2" superagent "^3.8.3" @@ -24128,6 +24138,11 @@ rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6: dependencies: bn.js "^5.2.0" +rlp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-3.0.0.tgz#5a60725ca4314a3a165feecca1836e4f2c1e2343" + integrity sha512-PD6U2PGk6Vq2spfgiWZdomLvRGDreBLxi5jv5M8EpRo3pU6VEm31KO+HFxE18Q3vgqfDrQ9pZA3FP95rkijNKw== + rn-host-detect@^1.1.5: version "1.2.0" resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0" @@ -25360,6 +25375,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" @@ -28445,3 +28468,8 @@ zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== + +zxcvbn@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" + integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=