Merge remote-tracking branch 'origin/develop' into master-sync

feature/default_network_editable
ryanml 3 years ago
commit 5a8ca9d16e
  1. 70
      .github/workflows/codeql-analysis.yml
  2. 2
      .prettierignore
  3. 14
      README.md
  4. 53
      app/_locales/en/messages.json
  5. 2
      app/_locales/zh_CN/messages.json
  6. 4
      app/build-types/beta/manifest/_base.json
  7. 4
      app/build-types/flask/manifest/_base.json
  8. 71
      app/scripts/background.js
  9. 110
      app/scripts/controllers/metametrics.js
  10. 22
      app/scripts/controllers/metametrics.test.js
  11. 15
      app/scripts/controllers/network/createInfuraClient.js
  12. 16
      app/scripts/controllers/network/createJsonRpcClient.js
  13. 5
      app/scripts/controllers/network/createMetamaskMiddleware.js
  14. 4
      app/scripts/controllers/network/network.js
  15. 11
      app/scripts/controllers/preferences.js
  16. 24
      app/scripts/controllers/preferences.test.js
  17. 2
      app/scripts/controllers/swaps.js
  18. 2
      app/scripts/controllers/threebox.js
  19. 5
      app/scripts/controllers/transactions/index.js
  20. 11
      app/scripts/controllers/transactions/index.test.js
  21. 14
      app/scripts/lib/decrypt-message-manager.js
  22. 14
      app/scripts/lib/encryption-public-key-manager.js
  23. 16
      app/scripts/lib/message-manager.js
  24. 5
      app/scripts/lib/message-manager.test.js
  25. 16
      app/scripts/lib/notification-manager.js
  26. 16
      app/scripts/lib/personal-message-manager.js
  27. 3
      app/scripts/lib/personal-message-manager.test.js
  28. 37
      app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
  29. 8
      app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js
  30. 3
      app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js
  31. 5
      app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js
  32. 7
      app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js
  33. 3
      app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js
  34. 17
      app/scripts/lib/typed-message-manager.js
  35. 1
      app/scripts/lib/typed-message-manager.test.js
  36. 35
      app/scripts/metamask-controller.js
  37. 4
      app/scripts/platforms/extension.js
  38. 10
      development/build/scripts.js
  39. 13
      development/generate-lavamoat-policies.sh
  40. 7
      development/verify-locale-strings.js
  41. 0
      lavamoat/browserify/beta/policy-override.json
  42. 5
      lavamoat/browserify/beta/policy.json
  43. 55
      lavamoat/browserify/flask/policy-override.json
  44. 4772
      lavamoat/browserify/flask/policy.json
  45. 55
      lavamoat/browserify/main/policy-override.json
  46. 4772
      lavamoat/browserify/main/policy.json
  47. 0
      lavamoat/build-system/policy-override.json
  48. 0
      lavamoat/build-system/policy.json
  49. 18
      package.json
  50. 4
      shared/constants/metametrics.js
  51. 10
      shared/constants/network.js
  52. 4
      test/data/mock-state.json
  53. 95
      test/e2e/tests/from-import-ui.spec.js
  54. 176
      test/e2e/tests/metamask-responsive-ui.spec.js
  55. 64
      test/e2e/tests/provider-api.spec.js
  56. 8
      test/e2e/webdriver/driver.js
  57. 7
      test/stub/provider.js
  58. 10
      ui/components/app/confirm-page-container/confirm-page-container-container.test.js
  59. 185
      ui/components/app/confirm-page-container/confirm-page-container.component.js
  60. 27
      ui/components/app/gas-timing/gas-timing.component.js
  61. 5
      ui/components/app/gas-timing/index.scss
  62. 4
      ui/components/app/modals/account-details-modal/account-details-modal.component.js
  63. 4
      ui/components/app/modals/account-details-modal/index.scss
  64. 2
      ui/components/app/modals/modal.js
  65. 45
      ui/components/app/signature-request-original/signature-request-original.component.js
  66. 6
      ui/components/app/signature-request-original/signature-request-original.container.js
  67. 32
      ui/components/app/signature-request/signature-request.component.js
  68. 13
      ui/components/app/signature-request/signature-request.container.js
  69. 51
      ui/components/app/transaction-detail/index.scss
  70. 83
      ui/components/app/transaction-detail/transaction-detail.component.js
  71. 94
      ui/components/app/transaction-detail/transaction-detail.component.test.js
  72. 42
      ui/components/ui/card/README.mdx
  73. 23
      ui/components/ui/card/card.component.js
  74. 21
      ui/components/ui/card/card.component.test.js
  75. 60
      ui/components/ui/card/card.js
  76. 169
      ui/components/ui/card/card.stories.js
  77. 11
      ui/components/ui/card/card.test.js
  78. 2
      ui/components/ui/card/index.js
  79. 11
      ui/components/ui/card/index.scss
  80. 15
      ui/components/ui/chip/README.mdx
  81. 3
      ui/components/ui/chip/chip-with-input.js
  82. 42
      ui/components/ui/chip/chip.js
  83. 5
      ui/components/ui/chip/chip.scss
  84. 125
      ui/components/ui/chip/chip.stories.js
  85. 3
      ui/components/ui/form-field/form-field.js
  86. 35
      ui/components/ui/qr-code/index.scss
  87. 24
      ui/components/ui/qr-code/qr-code.js
  88. 10
      ui/components/ui/token-input/token-input.component.js
  89. 1
      ui/components/ui/ui-components.scss
  90. 3
      ui/components/ui/unit-input/unit-input.component.js
  91. 37
      ui/contexts/gasFee.js
  92. 2
      ui/css/design-system/colors.scss
  93. 2
      ui/helpers/constants/routes.js
  94. 5
      ui/helpers/constants/transactions.js
  95. 7
      ui/hooks/gasFeeInput/useGasFeeErrors.js
  96. 32
      ui/hooks/gasFeeInput/useGasFeeInputs.js
  97. 65
      ui/pages/add-collectible/add-collectible.component.js
  98. 1
      ui/pages/add-collectible/index.js
  99. 40
      ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js
  100. 40
      ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ develop, Version-v*, cla-signatures, master, snaps ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
schedule:
- cron: '28 12 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

@ -1,5 +1,5 @@
node_modules/**
lavamoat/*/policy.json
lavamoat/**/policy.json
dist/**
builds/**
test-*/**

@ -67,9 +67,17 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
* The `allow-scripts` configuration in `package.json`
* Run `yarn allow-scripts auto` to update the `allow-scripts` configuration automatically. This config determines whether the package's install/postinstall scripts are allowed to run. Review each new package to determine whether the install script needs to run or not, testing if necessary.
* Unfortunately, `yarn allow-scripts auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.
* The LavaMoat auto-generated policy in `lavamoat/node/policy.json`
* Run `yarn lavamoat:auto` to re-generate this policy file. Review the changes to determine whether the access granted to each package seems appropriate.
* Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.
* The LavaMoat policy files. The _tl;dr_ is to run `yarn lavamoat:auto` to update these files, but there can be devils in the details. Continue reading for more information.
* There are two sets of LavaMoat policy files:
* The production LavaMoat policy files (`lavamoat/browserify/*/policy.json`), which are re-generated using `yarn lavamoat:background:auto`.
* These should be regenerated whenever the production dependencies for the background change.
* The build system LavaMoat policy file (`lavamoat/build-system/policy.json`), which is re-generated using `yarn lavamoat:build:auto`.
* This should be regenerated whenever the dependencies used by the build system itself change.
* Whenever you regenerate a policy file, review the changes to determine whether the access granted to each package seems appropriate.
* Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms.
macOS and Windows users may see extraneous changes relating to optional dependencies.
* Keep in mind that any kind of dynamic import or dynamic use of globals may elude LavaMoat's static analysis.
Refer to the LavaMoat documentation or ask for help if you run into any issues.
## Architecture

@ -43,6 +43,9 @@
"activityLog": {
"message": "activity log"
},
"add": {
"message": "Add"
},
"addANetwork": {
"message": "Add a network"
},
@ -100,6 +103,9 @@
"addToken": {
"message": "Add Token"
},
"address": {
"message": "Address"
},
"addressBookIcon": {
"message": "Address book icon"
},
@ -167,6 +173,14 @@
"message": "MetaMask",
"description": "The name of the application"
},
"appNameBeta": {
"message": "MetaMask Beta",
"description": "The name of the application (Beta)"
},
"appNameFlask": {
"message": "MetaMask Flask",
"description": "The name of the application (Flask)"
},
"approvalAndAggregatorTxFeeCost": {
"message": "Approval and aggregator network fee"
},
@ -549,6 +563,9 @@
"currentlyUnavailable": {
"message": "Unavailable on this network"
},
"custom": {
"message": "Advanced"
},
"customGas": {
"message": "Customize Gas"
},
@ -561,6 +578,13 @@
"customToken": {
"message": "Custom Token"
},
"dappSuggested": {
"message": "Site suggested"
},
"dappSuggestedTooltip": {
"message": "$1 has recommended this price.",
"description": "$1 represents the Dapp's origin"
},
"data": {
"message": "Data"
},
@ -891,6 +915,9 @@
"etherscanView": {
"message": "View account on Etherscan"
},
"etherscanViewOn": {
"message": "View on Etherscan"
},
"expandView": {
"message": "Expand view"
},
@ -1002,6 +1029,9 @@
"gasPriceInfoTooltipContent": {
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
},
"gasPriceLabel": {
"message": "Gas price"
},
"gasTimingMinutes": {
"message": "$1 minutes",
"description": "$1 represents a number of minutes"
@ -1100,9 +1130,15 @@
"hideZeroBalanceTokens": {
"message": "Hide Tokens Without Balance"
},
"high": {
"message": "Aggressive"
},
"history": {
"message": "History"
},
"id": {
"message": "ID"
},
"import": {
"message": "Import",
"description": "Button to import an account from a selected file"
@ -1117,7 +1153,7 @@
"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 "
"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"
@ -1336,6 +1372,12 @@
"lockTimeTooGreat": {
"message": "Lock time is too great"
},
"low": {
"message": "Low"
},
"lowPriorityMessage": {
"message": "Future transactions will queue after this one. This price was last seen was some time ago."
},
"mainnet": {
"message": "Ethereum Mainnet"
},
@ -1349,12 +1391,18 @@
"max": {
"message": "Max"
},
"maxBaseFee": {
"message": "Max base fee"
},
"maxFee": {
"message": "Max fee"
},
"maxPriorityFee": {
"message": "Max priority fee"
},
"medium": {
"message": "Market"
},
"memo": {
"message": "memo"
},
@ -1560,6 +1608,9 @@
"message": "Nonce is higher than suggested nonce of $1",
"description": "The next nonce according to MetaMask's internal logic"
},
"nftTokenIdPlaceholder": {
"message": "Enter the collectible ID"
},
"nfts": {
"message": "NFTs"
},

@ -503,7 +503,7 @@
"message": "编辑权限"
},
"encryptionPublicKeyNotice": {
"message": "$1 希望得到您的加密公钥。同意后该网站将可以您发送加密信息。",
"message": "$1 希望得到您的加密公钥。同意后该网站将可以您发送加密信息。",
"description": "$1 is the web3 site name"
},
"encryptionPublicKeyRequest": {

@ -21,6 +21,6 @@
"128": "images/icon-128.png",
"512": "images/icon-512.png"
},
"name": "__MSG_appName__ Beta",
"short_name": "__MSG_appName__ Beta"
"name": "__MSG_appNameBeta__",
"short_name": "__MSG_appNameBeta__"
}

@ -21,6 +21,6 @@
"128": "images/icon-128.png",
"512": "images/icon-512.png"
},
"name": "__MSG_appName__ Flask",
"short_name": "__MSG_appName__ Flask"
"name": "__MSG_appNameFlask__",
"short_name": "__MSG_appNameFlask__"
}

@ -17,13 +17,19 @@ import {
ENVIRONMENT_TYPE_FULLSCREEN,
} from '../../shared/constants/app';
import { SECOND } from '../../shared/constants/time';
import {
REJECT_NOTFICIATION_CLOSE,
REJECT_NOTFICIATION_CLOSE_SIG,
} from '../../shared/constants/metametrics';
import migrations from './migrations';
import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension';
import LocalStore from './lib/local-store';
import ReadOnlyNetworkStore from './lib/network-store';
import createStreamSink from './lib/createStreamSink';
import NotificationManager from './lib/notification-manager';
import NotificationManager, {
NOTIFICATION_MANAGER_EVENTS,
} from './lib/notification-manager';
import MetamaskController, {
METAMASK_CONTROLLER_EVENTS,
} from './metamask-controller';
@ -475,6 +481,69 @@ function setupController(initState, initLangCode) {
extension.browserAction.setBadgeBackgroundColor({ color: '#037DD6' });
}
notificationManager.on(
NOTIFICATION_MANAGER_EVENTS.POPUP_CLOSED,
rejectUnapprovedNotifications,
);
function rejectUnapprovedNotifications() {
Object.keys(
controller.txController.txStateManager.getUnapprovedTxList(),
).forEach((txId) =>
controller.txController.txStateManager.setTxStatusRejected(txId),
);
controller.messageManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.messageManager.rejectMsg(
tx.id,
REJECT_NOTFICIATION_CLOSE_SIG,
),
);
controller.personalMessageManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.personalMessageManager.rejectMsg(
tx.id,
REJECT_NOTFICIATION_CLOSE_SIG,
),
);
controller.typedMessageManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.typedMessageManager.rejectMsg(
tx.id,
REJECT_NOTFICIATION_CLOSE_SIG,
),
);
controller.decryptMessageManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.decryptMessageManager.rejectMsg(
tx.id,
REJECT_NOTFICIATION_CLOSE,
),
);
controller.encryptionPublicKeyManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.encryptionPublicKeyManager.rejectMsg(
tx.id,
REJECT_NOTFICIATION_CLOSE,
),
);
// We're specifcally avoid using approvalController directly for better
// Error support during rejection
Object.keys(
controller.permissionsController.approvals.state.pendingApprovals,
).forEach((approvalId) =>
controller.permissionsController.rejectPermissionsRequest(approvalId),
);
updateBadge();
}
return Promise.resolve();
}

@ -7,6 +7,14 @@ import {
METAMETRICS_BACKGROUND_PAGE_OBJECT,
} from '../../../shared/constants/metametrics';
const defaultCaptureException = (err) => {
// throw error on clean stack so its captured by platform integrations (eg sentry)
// but does not interupt the call stack
setTimeout(() => {
throw err;
});
};
/**
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsContext} MetaMetricsContext
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
@ -51,7 +59,9 @@ export default class MetaMetricsController {
version,
environment,
initState,
captureException = defaultCaptureException,
}) {
this._captureException = captureException;
const prefState = preferencesStore.getState();
this.chainId = getCurrentChainId();
this.network = getNetworkIdentifier();
@ -258,32 +268,52 @@ export default class MetaMetricsController {
* view
*/
trackPage({ name, params, environmentType, page, referrer }, options) {
if (this.state.participateInMetaMetrics === false) {
return;
}
try {
if (this.state.participateInMetaMetrics === false) {
return;
}
if (this.state.participateInMetaMetrics === null && !options?.isOptInPath) {
return;
if (
this.state.participateInMetaMetrics === null &&
!options?.isOptInPath
) {
return;
}
const { metaMetricsId } = this.state;
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this.segment.page({
[idTrait]: idValue,
name,
properties: {
params,
locale: this.locale,
network: this.network,
chain_id: this.chainId,
environment_type: environmentType,
},
context: this._buildContext(referrer, page),
});
} catch (err) {
this._captureException(err);
}
const { metaMetricsId } = this.state;
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this.segment.page({
[idTrait]: idValue,
name,
properties: {
params,
locale: this.locale,
network: this.network,
chain_id: this.chainId,
environment_type: environmentType,
},
context: this._buildContext(referrer, page),
});
}
/**
* track a metametrics event, performing necessary payload manipulation and
* submits a metametrics event, not waiting for it to complete or allowing its error to bubble up
* @param {MetaMetricsEventPayload} payload - details of the event
* @param {MetaMetricsEventOptions} [options] - options for handling/routing the event
*/
trackEvent(payload, options) {
// validation is not caught and handled
this.validatePayload(payload);
this.submitEvent(payload, options).catch((err) =>
this._captureException(err),
);
}
/**
* submits (or queues for submission) a metametrics event, performing necessary payload manipulation and
* routing the event to the appropriate segment source. Will split events
* with sensitiveProperties into two events, tracking the sensitiveProperties
* with the anonymousId only.
@ -291,21 +321,8 @@ export default class MetaMetricsController {
* @param {MetaMetricsEventOptions} [options] - options for handling/routing the event
* @returns {Promise<void>}
*/
async trackEvent(payload, options) {
// event and category are required fields for all payloads
if (!payload.event || !payload.category) {
throw new Error(
`Must specify event and category. Event was: ${
payload.event
}. Category was: ${payload.category}. Payload keys were: ${Object.keys(
payload,
)}. ${
typeof payload.properties === 'object'
? `Payload property keys were: ${Object.keys(payload.properties)}`
: ''
}`,
);
}
async submitEvent(payload, options) {
this.validatePayload(payload);
if (!this.state.participateInMetaMetrics && !options?.isOptIn) {
return;
@ -345,4 +362,25 @@ export default class MetaMetricsController {
await Promise.all(events);
}
/**
* validates a metametrics event
* @param {MetaMetricsEventPayload} payload - details of the event
*/
validatePayload(payload) {
// event and category are required fields for all payloads
if (!payload.event || !payload.category) {
throw new Error(
`Must specify event and category. Event was: ${
payload.event
}. Category was: ${payload.category}. Payload keys were: ${Object.keys(
payload,
)}. ${
typeof payload.properties === 'object'
? `Payload property keys were: ${Object.keys(payload.properties)}`
: ''
}`,
);
}
}
}

@ -196,14 +196,14 @@ describe('MetaMetricsController', function () {
});
});
describe('trackEvent', function () {
describe('submitEvent', function () {
it('should not track an event if user is not participating in metametrics', function () {
const mock = sinon.mock(segment);
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: false,
});
mock.expects('track').never();
metaMetricsController.trackEvent({
metaMetricsController.submitEvent({
event: 'Fake Event',
category: 'Unit Test',
properties: {
@ -230,7 +230,7 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES,
},
});
metaMetricsController.trackEvent(
metaMetricsController.submitEvent(
{
event: 'Fake Event',
category: 'Unit Test',
@ -260,7 +260,7 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES,
},
});
metaMetricsController.trackEvent(
metaMetricsController.submitEvent(
{
event: 'Fake Event',
category: 'Unit Test',
@ -289,7 +289,7 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES,
},
});
metaMetricsController.trackEvent(
metaMetricsController.submitEvent(
{
event: 'Fake Event',
category: 'Unit Test',
@ -317,7 +317,7 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES,
},
});
metaMetricsController.trackEvent({
metaMetricsController.submitEvent({
event: 'Fake Event',
category: 'Unit Test',
properties: {
@ -331,7 +331,7 @@ describe('MetaMetricsController', function () {
const metaMetricsController = getMetaMetricsController();
const flushStub = sinon.stub(segment, 'flush');
const flushCalled = waitUntilCalled(flushStub, segment);
metaMetricsController.trackEvent(
metaMetricsController.submitEvent(
{
event: 'Fake Event',
category: 'Unit Test',
@ -344,13 +344,13 @@ describe('MetaMetricsController', function () {
it('should throw if event or category not provided', function () {
const metaMetricsController = getMetaMetricsController();
assert.rejects(
() => metaMetricsController.trackEvent({ event: 'test' }),
() => metaMetricsController.submitEvent({ event: 'test' }),
/Must specify event and category\./u,
'must specify category',
);
assert.rejects(
() => metaMetricsController.trackEvent({ category: 'test' }),
() => metaMetricsController.submitEvent({ category: 'test' }),
/Must specify event and category\./u,
'must specify event',
);
@ -360,7 +360,7 @@ describe('MetaMetricsController', function () {
const metaMetricsController = getMetaMetricsController();
assert.rejects(
() =>
metaMetricsController.trackEvent(
metaMetricsController.submitEvent(
{
event: 'Fake Event',
category: 'Unit Test',
@ -375,7 +375,7 @@ describe('MetaMetricsController', function () {
it('should track sensitiveProperties in a separate, anonymous event', function () {
const metaMetricsController = getMetaMetricsController();
const spy = sinon.spy(segment, 'track');
metaMetricsController.trackEvent({
metaMetricsController.submitEvent({
event: 'Fake Event',
category: 'Unit Test',
sensitiveProperties: { foo: 'bar' },

@ -1,10 +1,13 @@
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
import createBlockRefMiddleware from 'eth-json-rpc-middleware/block-ref';
import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty';
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
import createInflightCacheMiddleware from 'eth-json-rpc-middleware/inflight-cache';
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import {
createBlockRefMiddleware,
createRetryOnEmptyMiddleware,
createBlockCacheMiddleware,
createInflightCacheMiddleware,
createBlockTrackerInspectorMiddleware,
providerFromMiddleware,
} from 'eth-json-rpc-middleware';
import createInfuraMiddleware from 'eth-json-rpc-infura';
import { PollingBlockTracker } from 'eth-block-tracker';

@ -1,10 +1,12 @@
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine';
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch';
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite';
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import {
createFetchMiddleware,
createBlockRefRewriteMiddleware,
createBlockCacheMiddleware,
createInflightCacheMiddleware,
createBlockTrackerInspectorMiddleware,
providerFromMiddleware,
} from 'eth-json-rpc-middleware';
import { PollingBlockTracker } from 'eth-block-tracker';
import { SECOND } from '../../../../shared/constants/time';
@ -27,7 +29,7 @@ export default function createJsonRpcClient({ rpcUrl, chainId }) {
createChainIdMiddleware(chainId),
createBlockRefRewriteMiddleware({ blockTracker }),
createBlockCacheMiddleware({ blockTracker }),
createInflightMiddleware(),
createInflightCacheMiddleware(),
createBlockTrackerInspectorMiddleware({ blockTracker }),
fetchMiddleware,
]);

@ -1,5 +1,5 @@
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet';
import { createWalletMiddleware } from 'eth-json-rpc-middleware';
import {
createPendingNonceMiddleware,
createPendingTxMiddleware,
@ -21,11 +21,10 @@ export default function createMetamaskMiddleware({
}) {
const metamaskMiddleware = mergeMiddleware([
createScaffoldMiddleware({
// staticSubprovider
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
}),
createWalletSubprovider({
createWalletMiddleware({
getAccounts,
processTransaction,
processEthSignMessage,

@ -2,7 +2,7 @@ import { strict as assert } from 'assert';
import EventEmitter from 'events';
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
import { JsonRpcEngine } from 'json-rpc-engine';
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine';
import { providerFromEngine } from 'eth-json-rpc-middleware';
import log from 'loglevel';
import {
createSwappableProxy,
@ -430,7 +430,7 @@ export default class NetworkController extends EventEmitter {
}
_setProviderAndBlockTracker({ provider, blockTracker }) {
// update or intialize proxies
// update or initialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider);
} else {

@ -37,6 +37,7 @@ export default class PreferencesController {
// set to true means the dynamic list from the API is being used
// set to false will be using the static list from contract-metadata
useTokenDetection: false,
advancedGasFee: null,
// WARNING: Do not use feature flags for security-sensitive things.
// Feature flag toggling is available in the global namespace
@ -129,6 +130,16 @@ export default class PreferencesController {
this.store.updateState({ useTokenDetection: val });
}
/**
* Setter for the `advancedGasFee` property
*
* @param {object} val - holds the maxBaseFee and PriorityFee that the user set as default advanced settings.
*
*/
setAdvancedGasFee(val) {
this.store.updateState({ advancedGasFee: val });
}
/**
* Add new methodData to state, to avoid requesting this information again through Infura
*

@ -266,4 +266,28 @@ describe('preferences controller', function () {
);
});
});
describe('setAdvancedGasFee', function () {
it('should default to null', function () {
const state = preferencesController.store.getState();
assert.equal(state.advancedGasFee, null);
});
it('should set the setAdvancedGasFee property in state', function () {
const state = preferencesController.store.getState();
assert.equal(state.advancedGasFee, null);
preferencesController.setAdvancedGasFee({
maxBaseFee: '1.5',
priorityFee: '2',
});
assert.equal(
preferencesController.store.getState().advancedGasFee.maxBaseFee,
'1.5',
);
assert.equal(
preferencesController.store.getState().advancedGasFee.priorityFee,
'2',
);
});
});
});

@ -280,7 +280,7 @@ export default class SwapsController {
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
// than 0, it means that approval has already occured and is not needed. Otherwise, for tokens to be swapped, a new
// than 0, it means that approval has already occurred and is not needed. Otherwise, for tokens to be swapped, a new
// call of the ERC-20 approve method is required.
approvalRequired =
allowance.eq(0) &&

@ -8,7 +8,7 @@ const Box = process.env.IN_TEST
import log from 'loglevel';
import { JsonRpcEngine } from 'json-rpc-engine';
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine';
import { providerFromEngine } from 'eth-json-rpc-middleware';
import Migrator from '../lib/migrator';
import migrations from '../migrations';
import createOriginMiddleware from '../lib/createOriginMiddleware';

@ -26,6 +26,7 @@ import {
TRANSACTION_TYPES,
TRANSACTION_ENVELOPE_TYPES,
} from '../../../../shared/constants/transaction';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import {
GAS_LIMITS,
@ -1447,8 +1448,8 @@ export default class TransactionController extends EventEmitter {
sensitiveProperties: {
status,
transaction_envelope_type: isEIP1559Transaction(txMeta)
? 'fee-market'
: 'legacy',
? TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET
: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
first_seen: time,
gas_limit: gasLimit,
...gasParamsInGwei,

@ -20,6 +20,7 @@ import {
GAS_ESTIMATE_TYPES,
GAS_RECOMMENDATIONS,
} from '../../../../shared/constants/gas';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import TransactionController, { TRANSACTION_EVENTS } from '.';
@ -774,7 +775,7 @@ describe('Transaction Controller', function () {
nonce: '0x4b',
},
type: TRANSACTION_TYPES.SIMPLE_SEND,
transaction_envelope_type: 'legacy',
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
origin: 'metamask',
chainId: currentChainId,
time: 1624408066355,
@ -1578,7 +1579,7 @@ describe('Transaction Controller', function () {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: 'legacy',
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
@ -1625,7 +1626,7 @@ describe('Transaction Controller', function () {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: 'legacy',
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
@ -1674,7 +1675,7 @@ describe('Transaction Controller', function () {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: 'legacy',
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
@ -1731,7 +1732,7 @@ describe('Transaction Controller', function () {
max_priority_fee_per_gas: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: 'fee-market',
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET,
status: 'unapproved',
estimate_suggested: GAS_RECOMMENDATIONS.MEDIUM,
estimate_used: GAS_RECOMMENDATIONS.HIGH,

@ -38,13 +38,14 @@ export default class DecryptMessageManager extends EventEmitter {
* @property {Array} messages Holds all messages that have been created by this DecryptMessageManager
*
*/
constructor() {
constructor(opts) {
super();
this.memStore = new ObservableStore({
unapprovedDecryptMsgs: {},
unapprovedDecryptMsgCount: 0,
});
this.messages = [];
this.metricsEvent = opts.metricsEvent;
}
/**
@ -237,7 +238,16 @@ export default class DecryptMessageManager extends EventEmitter {
* @param {number} msgId The id of the DecryptMessage to reject.
*
*/
rejectMsg(msgId) {
rejectMsg(msgId, reason = undefined) {
if (reason) {
this.metricsEvent({
event: reason,
category: 'Messages',
properties: {
action: 'Decrypt Message Request',
},
});
}
this._setMsgStatus(msgId, 'rejected');
}

@ -34,13 +34,14 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
* @property {Array} messages Holds all messages that have been created by this EncryptionPublicKeyManager
*
*/
constructor() {
constructor(opts) {
super();
this.memStore = new ObservableStore({
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
});
this.messages = [];
this.metricsEvent = opts.metricsEvent;
}
/**
@ -226,7 +227,16 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
* @param {number} msgId The id of the EncryptionPublicKey to reject.
*
*/
rejectMsg(msgId) {
rejectMsg(msgId, reason = undefined) {
if (reason) {
this.metricsEvent({
event: reason,
category: 'Messages',
properties: {
action: 'Encryption public key Request',
},
});
}
this._setMsgStatus(msgId, 'rejected');
}

@ -35,13 +35,14 @@ export default class MessageManager extends EventEmitter {
* @property {Array} messages Holds all messages that have been created by this MessageManager
*
*/
constructor() {
constructor({ metricsEvent }) {
super();
this.memStore = new ObservableStore({
unapprovedMsgs: {},
unapprovedMsgCount: 0,
});
this.messages = [];
this.metricsEvent = metricsEvent;
}
/**
@ -217,7 +218,18 @@ export default class MessageManager extends EventEmitter {
* @param {number} msgId - The id of the Message to reject.
*
*/
rejectMsg(msgId) {
rejectMsg(msgId, reason = undefined) {
if (reason) {
const msg = this.getMsg(msgId);
this.metricsEvent({
event: reason,
category: 'Transactions',
properties: {
action: 'Sign Request',
type: msg.type,
},
});
}
this._setMsgStatus(msgId, 'rejected');
}

@ -1,4 +1,5 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import MessageManager from './message-manager';
@ -6,7 +7,9 @@ describe('Message Manager', function () {
let messageManager;
beforeEach(function () {
messageManager = new MessageManager();
messageManager = new MessageManager({
metricsEvent: sinon.fake(),
});
});
describe('#getMsgList', function () {

@ -1,9 +1,14 @@
import EventEmitter from 'safe-event-emitter';
import ExtensionPlatform from '../platforms/extension';
const NOTIFICATION_HEIGHT = 620;
const NOTIFICATION_WIDTH = 360;
export default class NotificationManager {
export const NOTIFICATION_MANAGER_EVENTS = {
POPUP_CLOSED: 'onPopupClosed',
};
export default class NotificationManager extends EventEmitter {
/**
* A collection of methods for controlling the showing and hiding of the notification popup.
*
@ -12,7 +17,9 @@ export default class NotificationManager {
*/
constructor() {
super();
this.platform = new ExtensionPlatform();
this.platform.addOnRemovedListener(this._onWindowClosed.bind(this));
}
/**
@ -62,6 +69,13 @@ export default class NotificationManager {
}
}
_onWindowClosed(windowId) {
if (windowId === this._popupId) {
this._popupId = undefined;
this.emit(NOTIFICATION_MANAGER_EVENTS.POPUP_CLOSED);
}
}
/**
* Checks all open MetaMask windows, and returns the first one it finds that is a notification window (i.e. has the
* type 'popup')

@ -40,13 +40,14 @@ export default class PersonalMessageManager extends EventEmitter {
* @property {Array} messages Holds all messages that have been created by this PersonalMessageManager
*
*/
constructor() {
constructor({ metricsEvent }) {
super();
this.memStore = new ObservableStore({
unapprovedPersonalMsgs: {},
unapprovedPersonalMsgCount: 0,
});
this.messages = [];
this.metricsEvent = metricsEvent;
}
/**
@ -238,7 +239,18 @@ export default class PersonalMessageManager extends EventEmitter {
* @param {number} msgId - The id of the PersonalMessage to reject.
*
*/
rejectMsg(msgId) {
rejectMsg(msgId, reason = undefined) {
if (reason) {
const msg = this.getMsg(msgId);
this.metricsEvent({
event: reason,
category: 'Transactions',
properties: {
action: 'Sign Request',
type: msg.type,
},
});
}
this._setMsgStatus(msgId, 'rejected');
}

@ -1,4 +1,5 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import PersonalMessageManager from './personal-message-manager';
@ -6,7 +7,7 @@ describe('Personal Message Manager', function () {
let messageManager;
beforeEach(function () {
messageManager = new PersonalMessageManager();
messageManager = new PersonalMessageManager({ metricsEvent: sinon.fake() });
});
describe('#getMsgList', function () {

@ -1,8 +1,10 @@
import { ethErrors } from 'eth-rpc-errors';
import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network';
import handlers from './handlers';
const handlerMap = handlers.reduce((map, handler) => {
for (const methodName of handler.methodNames) {
map.set(methodName, handler.implementation);
map.set(methodName, handler);
}
return map;
}, new Map());
@ -21,14 +23,41 @@ const handlerMap = handlers.reduce((map, handler) => {
* Eventually, we'll want to extract this middleware into its own package.
*
* @param {Object} opts - The middleware options
* @param {Function} opts.sendMetrics - A function for sending a metrics event
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
*/
export default function createMethodMiddleware(opts) {
return function methodMiddleware(req, res, next, end) {
if (handlerMap.has(req.method)) {
return handlerMap.get(req.method)(req, res, next, end, opts);
// Reject unsupported methods.
if (UNSUPPORTED_RPC_METHODS.has(req.method)) {
return end(ethErrors.rpc.methodNotSupported());
}
const handler = handlerMap.get(req.method);
if (handler) {
const { implementation, hookNames } = handler;
return implementation(req, res, next, end, selectHooks(opts, hookNames));
}
return next();
};
}
/**
* Returns the subset of the specified `hooks` that are included in the
* `hookNames` object. This is a Principle of Least Authority (POLA) measure
* to ensure that each RPC method implementation only has access to the
* API "hooks" it needs to do its job.
*
* @param {Record<string, unknown>} hooks - The hooks to select from.
* @param {Record<string, true>} hookNames - The names of the hooks to select.
* @returns {Record<string, unknown> | undefined} The selected hooks.
*/
function selectHooks(hooks, hookNames) {
if (hookNames) {
return Object.keys(hookNames).reduce((hookSubset, hookName) => {
hookSubset[hookName] = hooks[hookName];
return hookSubset;
}, {});
}
return undefined;
}

@ -12,6 +12,14 @@ import { CHAIN_ID_TO_NETWORK_ID_MAP } from '../../../../../shared/constants/netw
const addEthereumChain = {
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
implementation: addEthereumChainHandler,
hookNames: {
addCustomRpc: true,
getCurrentChainId: true,
findCustomRpcBy: true,
updateRpcTarget: true,
requestUserApproval: true,
sendMetrics: true,
},
};
export default addEthereumChain;

@ -9,6 +9,9 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
const getProviderState = {
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
implementation: getProviderStateHandler,
hookNames: {
getProviderState: true,
},
};
export default getProviderState;

@ -10,6 +10,11 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
const logWeb3ShimUsage = {
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
implementation: logWeb3ShimUsageHandler,
hookNames: {
sendMetrics: true,
getWeb3ShimUsageState: true,
setWeb3ShimUsageRecorded: true,
},
};
export default logWeb3ShimUsage;

@ -15,6 +15,13 @@ import {
const switchEthereumChain = {
methodNames: [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN],
implementation: switchEthereumChainHandler,
hookNames: {
getCurrentChainId: true,
findCustomRpcBy: true,
setProviderType: true,
updateRpcTarget: true,
requestUserApproval: true,
},
};
export default switchEthereumChain;

@ -3,6 +3,9 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
const watchAsset = {
methodNames: [MESSAGE_TYPE.WATCH_ASSET, MESSAGE_TYPE.WATCH_ASSET_LEGACY],
implementation: watchAssetHandler,
hookNames: {
handleWatchAssetRequest: true,
},
};
export default watchAsset;

@ -32,7 +32,7 @@ export default class TypedMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
*/
constructor({ getCurrentChainId }) {
constructor({ getCurrentChainId, metricEvents }) {
super();
this._getCurrentChainId = getCurrentChainId;
this.memStore = new ObservableStore({
@ -40,6 +40,7 @@ export default class TypedMessageManager extends EventEmitter {
unapprovedTypedMessagesCount: 0,
});
this.messages = [];
this.metricEvents = metricEvents;
}
/**
@ -301,7 +302,19 @@ export default class TypedMessageManager extends EventEmitter {
* @param {number} msgId - The id of the TypedMessage to reject.
*
*/
rejectMsg(msgId) {
rejectMsg(msgId, reason = undefined) {
if (reason) {
const msg = this.getMsg(msgId);
this.metricsEvent({
event: reason,
category: 'Transactions',
properties: {
action: 'Sign Request',
version: msg.msgParams.version,
type: msg.type,
},
});
}
this._setMsgStatus(msgId, 'rejected');
}

@ -17,6 +17,7 @@ describe('Typed Message Manager', function () {
beforeEach(async function () {
typedMessageManager = new TypedMessageManager({
getCurrentChainId: sinon.fake.returns('0x1'),
metricsEvent: sinon.fake(),
});
msgParamsV1 = {

@ -7,7 +7,7 @@ import { debounce } from 'lodash';
import createEngineStream from 'json-rpc-middleware-stream/engineStream';
import createFilterMiddleware from 'eth-json-rpc-filters';
import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager';
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware';
import { providerAsMiddleware } from 'eth-json-rpc-middleware';
import KeyringController from 'eth-keyring-controller';
import { Mutex } from 'await-semaphore';
import { stripHexPrefix } from 'ethereumjs-util';
@ -17,6 +17,7 @@ import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring';
import LatticeKeyring from 'eth-lattice-keyring';
import EthQuery from 'eth-query';
import nanoid from 'nanoid';
import { captureException } from '@sentry/browser';
import {
AddressBookController,
ApprovalController,
@ -190,6 +191,7 @@ export default class MetamaskController extends EventEmitter {
version: this.platform.getVersion(),
environment: process.env.METAMASK_ENVIRONMENT,
initState: initState.MetaMetricsController,
captureException,
});
const gasFeeMessenger = this.controllerMessenger.getRestricted({
@ -524,14 +526,33 @@ export default class MetamaskController extends EventEmitter {
}
});
this.networkController.lookupNetwork();
this.messageManager = new MessageManager();
this.personalMessageManager = new PersonalMessageManager();
this.decryptMessageManager = new DecryptMessageManager();
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager();
this.messageManager = new MessageManager({
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.personalMessageManager = new PersonalMessageManager({
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.decryptMessageManager = new DecryptMessageManager({
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager({
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.typedMessageManager = new TypedMessageManager({
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.swapsController = new SwapsController({
@ -922,6 +943,10 @@ export default class MetamaskController extends EventEmitter {
this.preferencesController.setDismissSeedBackUpReminder,
this.preferencesController,
),
setAdvancedGasFee: nodeify(
preferencesController.setAdvancedGasFee,
preferencesController,
),
// AddressController
setAddressBook: nodeify(

@ -162,6 +162,10 @@ export default class ExtensionPlatform {
}
}
addOnRemovedListener(listener) {
extension.windows.onRemoved.addListener(listener);
}
getAllWindows() {
return new Promise((resolve, reject) => {
extension.windows.getAll((windows) => {

@ -358,10 +358,14 @@ function createFactoredBuild({
// lavamoat will add lavapack but it will be removed by bify-module-groups
// we will re-add it later by installing a lavapack runtime
const lavamoatOpts = {
policy: path.resolve(__dirname, '../../lavamoat/browserify/policy.json'),
policy: path.resolve(
__dirname,
`../../lavamoat/browserify/${buildType}/policy.json`,
),
policyName: buildType,
policyOverride: path.resolve(
__dirname,
'../../lavamoat/browserify/policy-override.json',
`../../lavamoat/browserify/${buildType}/policy-override.json`,
),
writeAutoPolicy: process.env.WRITE_AUTO_POLICY,
};
@ -456,7 +460,7 @@ function createFactoredBuild({
groupSet,
commonSet,
browserPlatforms,
useLavamoat: false,
useLavamoat: true,
});
break;
}

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
# Generate LavaMoat policies for the extension background script for each build
# type.
# ATTN: This may tax your device when running it locally.
concurrently --kill-others-on-fail -n main,beta,flask \
"WRITE_AUTO_POLICY=1 yarn dist" \
"WRITE_AUTO_POLICY=1 yarn dist --build-type beta" \
"WRITE_AUTO_POLICY=1 yarn dist --build-type flask"

@ -216,7 +216,12 @@ async function verifyEnglishLocale() {
}
// never consider these messages as unused
const messageExceptions = ['appName', 'appDescription'];
const messageExceptions = [
'appName',
'appNameBeta',
'appNameFlask',
'appDescription',
];
const englishMessages = Object.keys(englishLocale);
const unusedMessages = englishMessages.filter(

@ -325,6 +325,7 @@
"@ethersproject/bignumber": true,
"@ethersproject/bytes": true,
"@ethersproject/keccak256": true,
"@ethersproject/logger": true,
"@ethersproject/sha2": true,
"@ethersproject/strings": true
}
@ -1555,11 +1556,15 @@
},
"eth-json-rpc-middleware": {
"globals": {
"URL": true,
"btoa": true,
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/safe-event-emitter": true,
"browser-resolve": true,
"btoa": true,
"clone": true,
"eth-rpc-errors": true,

@ -0,0 +1,55 @@
{
"resources": {
"browser-resolve": {
"packages": {
"core-js": true
}
},
"babel-runtime": {
"packages": {
"@babel/runtime": true
}
},
"node-fetch": {
"globals": {
"fetch": true
}
},
"lodash": {
"globals": {
"setTimeout": true,
"clearTimeout": true
}
},
"@ethersproject/random": {
"globals": {
"crypto.getRandomValues": true
}
},
"browser-passworder": {
"globals": {
"crypto": true
}
},
"randombytes": {
"globals": {
"crypto.getRandomValues": true
}
},
"extensionizer": {
"globals": {
"console": true
}
},
"web3": {
"globals": {
"XMLHttpRequest": true
}
},
"storage": {
"globals": {
"localStorage": true
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,55 @@
{
"resources": {
"browser-resolve": {
"packages": {
"core-js": true
}
},
"babel-runtime": {
"packages": {
"@babel/runtime": true
}
},
"node-fetch": {
"globals": {
"fetch": true
}
},
"lodash": {
"globals": {
"setTimeout": true,
"clearTimeout": true
}
},
"@ethersproject/random": {
"globals": {
"crypto.getRandomValues": true
}
},
"browser-passworder": {
"globals": {
"crypto": true
}
},
"randombytes": {
"globals": {
"crypto.getRandomValues": true
}
},
"extensionizer": {
"globals": {
"console": true
}
},
"web3": {
"globals": {
"XMLHttpRequest": true
}
},
"storage": {
"globals": {
"localStorage": true
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -12,7 +12,7 @@
"start": "yarn build:dev dev",
"start:lavamoat": "yarn build dev",
"dist": "yarn build prod",
"build": "lavamoat development/build/index.js",
"build": "yarn lavamoat:build",
"build:dev": "node development/build/index.js",
"start:test": "yarn build testDev",
"benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js",
@ -41,8 +41,9 @@
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
"ganache:start": "./development/run-ganache.sh",
"sentry:publish": "node ./development/sentry-publish.js",
"lint": "prettier --check '**/*.json' && eslint . --ext js,snap --cache && yarn lint:styles",
"lint:fix": "prettier --write '**/*.json' && eslint . --ext js --cache --fix",
"lint:prettier": "prettier '**/*.json'",
"lint": "yarn lint:prettier --check '**/*.json' && eslint . --ext js,snap --cache && yarn lint:styles",
"lint:fix": "yarn lint:prettier --write '**/*.json' && eslint . --ext js --cache --fix",
"lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint",
"lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint --fix",
"lint:changelog": "auto-changelog validate",
@ -63,9 +64,10 @@
"storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master",
"update-changelog": "auto-changelog update",
"generate:migration": "./development/generate-migration.sh",
"lavamoat:build:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
"lavamoat:debug:build": "lavamoat ./development/build/index.js --writeAutoPolicyDebug",
"lavamoat:background:auto": "WRITE_AUTO_POLICY=1 yarn build prod",
"lavamoat:build": "lavamoat development/build/index.js --policy lavamoat/build-system/policy.json --policyOverride lavamoat/build-system/policy-override.json",
"lavamoat:build:auto": "yarn lavamoat:build --writeAutoPolicy",
"lavamoat:debug:build": "yarn lavamoat:build --writeAutoPolicyDebug --policydebug lavamoat/build-system/policy-debug.json",
"lavamoat:background:auto": "./development/generate-lavamoat-policies.sh",
"lavamoat:auto": "yarn lavamoat:build:auto && yarn lavamoat:background:auto"
},
"resolutions": {
@ -135,10 +137,10 @@
"eth-ens-namehash": "^2.0.8",
"eth-json-rpc-filters": "^4.2.1",
"eth-json-rpc-infura": "^5.1.0",
"eth-json-rpc-middleware": "^6.0.0",
"eth-json-rpc-middleware": "^8.0.0",
"eth-keyring-controller": "^6.2.0",
"eth-lattice-keyring": "^0.4.0",
"eth-method-registry": "^2.0.0",
"eth-lattice-keyring": "^0.4.0",
"eth-query": "^2.1.2",
"eth-rpc-errors": "^4.0.2",
"eth-sig-util": "^3.0.0",

@ -140,3 +140,7 @@ export const METAMETRICS_BACKGROUND_PAGE_OBJECT = {
* @property {() => void} identify - Identify an anonymous user. We do not
* currently use this method.
*/
export const REJECT_NOTFICIATION_CLOSE = 'Cancel Via Notification Close';
export const REJECT_NOTFICIATION_CLOSE_SIG =
'Cancel Sig Request Via Notification Close';

@ -161,3 +161,13 @@ export const CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP = {
[OPTIMISM_CHAIN_ID]: 1,
[OPTIMISM_TESTNET_CHAIN_ID]: 1,
};
/**
* Ethereum JSON-RPC methods that are known to exist but that we intentionally
* do not support.
*/
export const UNSUPPORTED_RPC_METHODS = new Set([
// This is implemented later in our middleware stack – specifically, in
// eth-json-rpc-middleware – but our UI does not support it.
'eth_signTransaction',
]);

@ -160,6 +160,10 @@
"toNickname": ""
},
"useTokenDetection": true,
"advancedGasFee": {
"maxBaseFee": "1.5",
"priorityFee": "2"
},
"tokenList": {
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",

@ -27,38 +27,72 @@ describe('Metamask Import UI', function () {
async ({ driver }) => {
await driver.navigate();
// clicks the continue button on the welcome screen
await driver.findElement('.welcome-page__header');
await driver.clickElement({
text: enLocaleMessages.getStarted.message,
tag: 'button',
});
if (process.env.ONBOARDING_V2 === '1') {
// welcome
await driver.clickElement('[data-testid="onboarding-import-wallet"]');
// clicks the "Import Wallet" option
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
// metrics
await driver.clickElement('[data-testid="metametrics-no-thanks"]');
// clicks the "No thanks" option on the metametrics opt-in screen
await driver.clickElement('.btn-secondary');
// import with recovery phrase
await driver.fill('[data-testid="import-srp-text"]', testSeedPhrase);
await driver.clickElement('[data-testid="import-srp-confirm"]');
// Import Secret Recovery Phrase
await driver.fill(
'input[placeholder="Paste Secret Recovery Phrase from clipboard"]',
testSeedPhrase,
);
// create password
await driver.fill(
'[data-testid="create-password-new"]',
'correct horse battery staple',
);
await driver.fill(
'[data-testid="create-password-confirm"]',
'correct horse battery staple',
);
await driver.clickElement('[data-testid="create-password-terms"]');
await driver.clickElement('[data-testid="create-password-import"]');
// complete
await driver.clickElement('[data-testid="onboarding-complete-done"]');
// pin extension
await driver.clickElement('[data-testid="pin-extension-next"]');
await driver.clickElement('[data-testid="pin-extension-done"]');
} else {
// clicks the continue button on the welcome screen
await driver.findElement('.welcome-page__header');
await driver.clickElement({
text: enLocaleMessages.getStarted.message,
tag: 'button',
});
// clicks the "Import Wallet" option
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
// clicks the "No thanks" option on the metametrics opt-in screen
await driver.clickElement('.btn-secondary');
// Import Secret Recovery Phrase
await driver.fill(
'input[placeholder="Paste Secret Recovery Phrase from clipboard"]',
testSeedPhrase,
);
await driver.fill('#password', 'correct horse battery staple');
await driver.fill('#confirm-password', 'correct horse battery staple');
await driver.fill('#password', 'correct horse battery staple');
await driver.fill(
'#confirm-password',
'correct horse battery staple',
);
await driver.clickElement('.first-time-flow__terms');
await driver.clickElement('.first-time-flow__terms');
await driver.clickElement({ text: 'Import', tag: 'button' });
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',
});
// clicks through the success screen
await driver.findElement({ text: 'Congratulations', tag: 'div' });
await driver.clickElement({
text: enLocaleMessages.endOfFlowMessage10.message,
tag: 'button',
});
}
// Show account information
await driver.clickElement(
@ -233,10 +267,15 @@ describe('Metamask Import UI', function () {
// should remove the account
await driver.clickElement({ text: 'Remove', tag: 'button' });
const currentActiveAccountName = await driver.findElement(
'.selected-account__name',
// Wait until selected account switches away from removed account to first account
await driver.waitForSelector(
{
css: '.selected-account__name',
text: 'Account 1',
},
{ timeout: 10000 },
);
assert.equal(await currentActiveAccountName.getText(), 'Account 1');
await driver.delay(regularDelayMs);
await driver.clickElement('.account-menu__icon');

@ -16,77 +16,133 @@ describe('Metamask Responsive UI', function () {
async ({ driver }) => {
await driver.navigate();
// clicks the continue button on the welcome screen
await driver.findElement('.welcome-page__header');
await driver.clickElement({
text: enLocaleMessages.getStarted.message,
tag: 'button',
});
await driver.delay(tinyDelayMs);
async function clickWordAndWait(word) {
await driver.clickElement(
`[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`,
);
await driver.delay(tinyDelayMs);
}
// clicks the "Create New Wallet" option
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
if (process.env.ONBOARDING_V2 === '1') {
// welcome
await driver.clickElement('[data-testid="onboarding-create-wallet"]');
// clicks the "I Agree" option on the metametrics opt-in screen
await driver.clickElement('.btn-primary');
// metrics
await driver.clickElement('[data-testid="metametrics-no-thanks"]');
// accepts a secure password
await driver.fill(
'.first-time-flow__form #create-password',
'correct horse battery staple',
);
await driver.fill(
'.first-time-flow__form #confirm-password',
'correct horse battery staple',
);
await driver.clickElement('.first-time-flow__checkbox');
await driver.clickElement('.first-time-flow__form button');
// 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-wallet"]');
// renders the Secret Recovery Phrase intro screen
await driver.clickElement('.seed-phrase-intro__left button');
// secure wallet
await driver.clickElement(
'[data-testid="secure-wallet-recommended"]',
);
// reveals the Secret Recovery Phrase
await driver.clickElement(
'.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button',
);
const revealedSeedPhrase = await driver.findElement(
'.reveal-seed-phrase__secret-words',
);
const seedPhrase = await revealedSeedPhrase.getText();
assert.equal(seedPhrase.split(' ').length, 12);
// review
await driver.clickElement('[data-testid="recovery-phrase-reveal"]');
const chipTwo = await (
await driver.findElement('[data-testid="recovery-phrase-chip-2"]')
).getText();
const chipThree = await (
await driver.findElement('[data-testid="recovery-phrase-chip-3"]')
).getText();
const chipSeven = await (
await driver.findElement('[data-testid="recovery-phrase-chip-7"]')
).getText();
await driver.clickElement('[data-testid="recovery-phrase-next"]');
// confirm
await driver.fill('[data-testid="recovery-phrase-input-2"]', chipTwo);
await driver.fill(
'[data-testid="recovery-phrase-input-3"]',
chipThree,
);
await driver.fill(
'[data-testid="recovery-phrase-input-7"]',
chipSeven,
);
await driver.clickElement('[data-testid="recovery-phrase-confirm"]');
// 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',
});
await driver.delay(tinyDelayMs);
await driver.clickElement({
text: enLocaleMessages.next.message,
tag: 'button',
});
// clicks the "Create New Wallet" option
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
async function clickWordAndWait(word) {
await driver.clickElement(
`[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`,
// clicks the "I Agree" option on the metametrics opt-in screen
await driver.clickElement('.btn-primary');
// accepts a secure password
await driver.fill(
'.first-time-flow__form #create-password',
'correct horse battery staple',
);
await driver.delay(tinyDelayMs);
}
await driver.fill(
'.first-time-flow__form #confirm-password',
'correct horse battery staple',
);
await driver.clickElement('.first-time-flow__checkbox');
await driver.clickElement('.first-time-flow__form button');
// can retype the Secret Recovery Phrase
const words = seedPhrase.split(' ');
for (const word of words) {
await clickWordAndWait(word);
}
await driver.clickElement({ text: 'Confirm', tag: 'button' });
// renders the Secret Recovery Phrase intro screen
await driver.clickElement('.seed-phrase-intro__left button');
// clicks through the success screen
await driver.findElement({ text: 'Congratulations', tag: 'div' });
await driver.clickElement({
text: enLocaleMessages.endOfFlowMessage10.message,
tag: 'button',
});
// reveals the Secret Recovery Phrase
await driver.clickElement(
'.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button',
);
const revealedSeedPhrase = await driver.findElement(
'.reveal-seed-phrase__secret-words',
);
const seedPhrase = await revealedSeedPhrase.getText();
assert.equal(seedPhrase.split(' ').length, 12);
await driver.clickElement({
text: enLocaleMessages.next.message,
tag: 'button',
});
// can retype the Secret Recovery Phrase
const words = seedPhrase.split(' ');
for (const word of words) {
await clickWordAndWait(word);
}
await driver.clickElement({ text: 'Confirm', tag: 'button' });
// clicks through the success screen
await driver.findElement({ text: 'Congratulations', tag: 'div' });
await driver.clickElement({
text: enLocaleMessages.endOfFlowMessage10.message,
tag: 'button',
});
}
// Show account information
// balance renders
await driver.waitForSelector({
css: '[data-testid="eth-overview__primary-currency"]',
text: '0 ETH',
});
// assert balance
const balance = await driver.findElement(
'[data-testid="wallet-balance"]',
);
assert.ok(/^0\sETH$/u.test(await balance.getText()));
},
);
});

@ -1,17 +1,19 @@
const { strict: assert } = require('assert');
const { errorCodes } = require('eth-rpc-errors');
const { withFixtures } = require('../helpers');
describe('MetaMask', function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: 25000000000000000000,
},
],
};
it('provider should inform dapp when switching networks', async function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: 25000000000000000000,
},
],
};
await withFixtures(
{
dapp: true,
@ -62,4 +64,48 @@ describe('MetaMask', function () {
},
);
});
it('should reject unsupported methods', async function () {
await withFixtures(
{
dapp: true,
failOnConsoleError: false,
fixtures: 'connected-state',
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.openNewPage('http://127.0.0.1:8080/');
for (const unsupportedMethod of ['eth_signTransaction']) {
assert.equal(
await driver.executeAsyncScript(`
const webDriverCallback = arguments[arguments.length - 1];
window.ethereum.request({ method: '${unsupportedMethod}' })
.then(() => {
console.error('The unsupported method "${unsupportedMethod}" was not rejected.');
webDriverCallback(false);
})
.catch((error) => {
if (error.code === ${errorCodes.rpc.methodNotSupported}) {
webDriverCallback(true);
}
console.error(
'The unsupported method "${unsupportedMethod}" was rejected with an unexpected error.',
error,
);
webDriverCallback(false);
})
`),
true,
`The unsupported method "${unsupportedMethod}" should be rejected by the provider.`,
);
}
},
);
});
});

@ -29,6 +29,10 @@ function wrapElementWithAPI(element, driver) {
return element;
}
/**
* For Selenium WebDriver API documentation, see:
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
*/
class Driver {
/**
* @param {!ThenableWebDriver} driver - A {@code WebDriver} instance
@ -49,6 +53,10 @@ class Driver {
};
}
async executeAsyncScript(script, ...args) {
return this.driver.executeAsyncScript(script, args);
}
async executeScript(script, ...args) {
return this.driver.executeScript(script, args);
}

@ -1,6 +1,5 @@
import { JsonRpcEngine } from 'json-rpc-engine';
import scaffoldMiddleware from 'eth-json-rpc-middleware/scaffold';
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware';
import { JsonRpcEngine, createScaffoldMiddleware } from 'json-rpc-engine';
import { providerAsMiddleware } from 'eth-json-rpc-middleware';
import GanacheCore from 'ganache-core';
export function getTestSeed() {
@ -45,7 +44,7 @@ export function providerFromEngine(engine) {
export function createTestProviderTools(opts = {}) {
const engine = createEngineForTestData();
// handle provided hooks
engine.push(scaffoldMiddleware(opts.scaffold || {}));
engine.push(createScaffoldMiddleware(opts.scaffold || {}));
// handle block tracker methods
engine.push(
providerAsMiddleware(

@ -10,6 +10,14 @@ import ConfirmPageContainer, {
ConfirmPageContainerNavigation,
} from '.';
jest.mock('../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
}));
describe('Confirm Page Container Container Test', () => {
let wrapper;
@ -31,6 +39,8 @@ describe('Confirm Page Container Container Test', () => {
selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5',
addressBook: [],
chainId: 'test',
identities: [],
featureFlags: {},
},
};

@ -4,6 +4,7 @@ import SenderToRecipient from '../../ui/sender-to-recipient';
import { PageContainerFooter } from '../../ui/page-container';
import EditGasPopover from '../edit-gas-popover';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import ErrorMessage from '../../ui/error-message';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
import Dialog from '../../ui/dialog';
@ -135,102 +136,104 @@ export default class ConfirmPageContainer extends Component {
currentTransaction.txParams?.value === '0x0';
return (
<div className="page-container">
<ConfirmPageContainerNavigation
totalTx={totalTx}
positionOfCurrentTx={positionOfCurrentTx}
nextTxId={nextTxId}
prevTxId={prevTxId}
showNavigation={showNavigation}
onNextTx={(txId) => onNextTx(txId)}
firstTx={firstTx}
lastTx={lastTx}
ofText={ofText}
requestsWaitingText={requestsWaitingText}
/>
<ConfirmPageContainerHeader
showEdit={showEdit}
onEdit={() => onEdit()}
showAccountInHeader={showAccountInHeader}
accountAddress={fromAddress}
>
{hideSenderToRecipient ? null : (
<SenderToRecipient
senderName={fromName}
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
recipientEns={toEns}
recipientNickname={toNickname}
<GasFeeContextProvider transaction={currentTransaction}>
<div className="page-container">
<ConfirmPageContainerNavigation
totalTx={totalTx}
positionOfCurrentTx={positionOfCurrentTx}
nextTxId={nextTxId}
prevTxId={prevTxId}
showNavigation={showNavigation}
onNextTx={(txId) => onNextTx(txId)}
firstTx={firstTx}
lastTx={lastTx}
ofText={ofText}
requestsWaitingText={requestsWaitingText}
/>
<ConfirmPageContainerHeader
showEdit={showEdit}
onEdit={() => onEdit()}
showAccountInHeader={showAccountInHeader}
accountAddress={fromAddress}
>
{hideSenderToRecipient ? null : (
<SenderToRecipient
senderName={fromName}
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
recipientEns={toEns}
recipientNickname={toNickname}
/>
)}
</ConfirmPageContainerHeader>
<div>
{showAddToAddressDialog && (
<Dialog
type="message"
className="send__dialog"
onClick={() => showAddToAddressBookModal()}
>
{this.context.t('newAccountDetectedDialogMessage')}
</Dialog>
)}
</div>
{contentComponent || (
<ConfirmPageContainerContent
action={action}
title={title}
titleComponent={titleComponent}
subtitleComponent={subtitleComponent}
hideSubtitle={hideSubtitle}
detailsComponent={detailsComponent}
dataComponent={dataComponent}
errorMessage={errorMessage}
errorKey={errorKey}
identiconAddress={identiconAddress}
nonce={nonce}
warning={warning}
onCancelAll={onCancelAll}
onCancel={onCancel}
cancelText={this.context.t('reject')}
onSubmit={onSubmit}
submitText={this.context.t('confirm')}
disabled={disabled}
unapprovedTxCount={unapprovedTxCount}
rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])}
origin={origin}
ethGasPriceWarning={ethGasPriceWarning}
hideTitle={hideTitle}
/>
)}
</ConfirmPageContainerHeader>
<div>
{showAddToAddressDialog && (
<Dialog
type="message"
className="send__dialog"
onClick={() => showAddToAddressBookModal()}
{shouldDisplayWarning && (
<div className="confirm-approve-content__warning">
<ErrorMessage errorKey={errorKey} />
</div>
)}
{contentComponent && (
<PageContainerFooter
onCancel={onCancel}
cancelText={this.context.t('reject')}
onSubmit={onSubmit}
submitText={this.context.t('confirm')}
disabled={disabled}
>
{this.context.t('newAccountDetectedDialogMessage')}
</Dialog>
{unapprovedTxCount > 1 && (
<a onClick={onCancelAll}>
{this.context.t('rejectTxsN', [unapprovedTxCount])}
</a>
)}
</PageContainerFooter>
)}
{editingGas && (
<EditGasPopover
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
onClose={handleCloseEditGas}
transaction={currentTransaction}
/>
)}
</div>
{contentComponent || (
<ConfirmPageContainerContent
action={action}
title={title}
titleComponent={titleComponent}
subtitleComponent={subtitleComponent}
hideSubtitle={hideSubtitle}
detailsComponent={detailsComponent}
dataComponent={dataComponent}
errorMessage={errorMessage}
errorKey={errorKey}
identiconAddress={identiconAddress}
nonce={nonce}
warning={warning}
onCancelAll={onCancelAll}
onCancel={onCancel}
cancelText={this.context.t('reject')}
onSubmit={onSubmit}
submitText={this.context.t('confirm')}
disabled={disabled}
unapprovedTxCount={unapprovedTxCount}
rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])}
origin={origin}
ethGasPriceWarning={ethGasPriceWarning}
hideTitle={hideTitle}
/>
)}
{shouldDisplayWarning && (
<div className="confirm-approve-content__warning">
<ErrorMessage errorKey={errorKey} />
</div>
)}
{contentComponent && (
<PageContainerFooter
onCancel={onCancel}
cancelText={this.context.t('reject')}
onSubmit={onSubmit}
submitText={this.context.t('confirm')}
disabled={disabled}
>
{unapprovedTxCount > 1 && (
<a onClick={onCancelAll}>
{this.context.t('rejectTxsN', [unapprovedTxCount])}
</a>
)}
</PageContainerFooter>
)}
{editingGas && (
<EditGasPopover
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
onClose={handleCloseEditGas}
transaction={currentTransaction}
/>
)}
</div>
</GasFeeContextProvider>
);
}
}

@ -24,6 +24,7 @@ import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
import { getGasFeeTimeEstimate } from '../../../store/actions';
import { GAS_FORM_ERRORS } from '../../../helpers/constants/gas';
import { useGasFeeContext } from '../../../contexts/gasFee';
// Once we reach this second threshold, we switch to minutes as a unit
const SECOND_CUTOFF = 90;
@ -49,6 +50,7 @@ export default function GasTiming({
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
const t = useContext(I18nContext);
const { estimateToUse } = useGasFeeContext();
// If the user has chosen a value lower than the low gas fee estimate,
// We'll need to use the useEffect hook below to make a call to calculate
@ -94,12 +96,17 @@ export default function GasTiming({
previousIsUnknownLow,
]);
const unknownProcessingTimeText = (
<>
{t('editGasTooLow')}{' '}
<InfoTooltip position="top" contentText={t('editGasTooLowTooltip')} />
</>
);
let unknownProcessingTimeText;
if (EIP_1559_V2) {
unknownProcessingTimeText = t('editGasTooLow');
} else {
unknownProcessingTimeText = (
<>
{t('editGasTooLow')}{' '}
<InfoTooltip position="top" contentText={t('editGasTooLowTooltip')} />
</>
);
}
if (
gasWarnings?.maxPriorityFee === GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW ||
@ -148,8 +155,9 @@ export default function GasTiming({
]);
}
} else {
attitude = 'negative';
if (!EIP_1559_V2 || estimateToUse === 'low') {
attitude = 'negative';
}
// If the user has chosen a value less than our low estimate,
// calculate a potential wait time
if (isUnknownLow) {
@ -191,7 +199,8 @@ export default function GasTiming({
<Typography
variant={TYPOGRAPHY.H7}
className={classNames('gas-timing', {
[`gas-timing--${attitude}`]: attitude,
[`gas-timing--${attitude}`]: attitude && !EIP_1559_V2,
[`gas-timing--${attitude}-V2`]: attitude && EIP_1559_V2,
})}
>
{text}

@ -14,6 +14,11 @@
font-weight: bold;
}
&--negative-V2 {
color: $secondary-1;
font-weight: bold;
}
.info-tooltip {
display: inline-block;
margin-inline-start: 4px;

@ -84,9 +84,7 @@ export default class AccountDetailsModal extends Component {
? this.context.t('blockExplorerView', [
getURLHostName(rpcPrefs.blockExplorerUrl),
])
: this.context.t('viewOnEtherscan', [
this.context.t('blockExplorerAccountAction'),
])}
: this.context.t('etherscanViewOn')}
</Button>
{exportPrivateKeyFeatureEnabled ? (

@ -8,13 +8,13 @@
& &__button {
margin-top: 17px;
padding: 10px 22px;
width: 286px;
width: 284px;
}
&__divider {
width: 100%;
height: 1px;
margin: 19px 0 8px 0;
margin: 16px 0 8px 0;
background-color: $alto;
}

@ -64,7 +64,7 @@ const accountModalStyle = {
margin: '0 auto',
},
laptopModalStyle: {
width: '360px',
width: '335px',
// top: 'calc(33% + 45px)',
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
borderRadius: '4px',

@ -5,11 +5,7 @@ import classnames from 'classnames';
import { ObjectInspector } from 'react-inspector';
import LedgerInstructionField from '../ledger-instruction-field';
import {
ENVIRONMENT_TYPE_NOTIFICATION,
MESSAGE_TYPE,
} from '../../../../shared/constants/app';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import Identicon from '../../ui/identicon';
import AccountListItem from '../account-list-item';
import { conversionUtil } from '../../../../shared/modules/conversion.utils';
@ -39,42 +35,13 @@ export default class SignatureRequestOriginal extends Component {
domainMetadata: PropTypes.object,
hardwareWalletRequiresConnection: PropTypes.bool,
isLedgerWallet: PropTypes.bool,
nativeCurrency: PropTypes.string.isRequired,
};
state = {
fromAccount: this.props.fromAccount,
};
componentDidMount = () => {
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload);
}
};
componentWillUnmount = () => {
this._removeBeforeUnload();
};
_beforeUnload = (event) => {
const { clearConfirmTransaction, cancel } = this.props;
const { metricsEvent } = this.context;
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Cancel Sig Request Via Notification Close',
},
});
clearConfirmTransaction();
cancel(event);
};
_removeBeforeUnload = () => {
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
window.removeEventListener('beforeunload', this._beforeUnload);
}
};
renderHeader = () => {
return (
<div className="request-signature__header">
@ -108,12 +75,12 @@ export default class SignatureRequestOriginal extends Component {
};
renderBalance = () => {
const { conversionRate } = this.props;
const { conversionRate, nativeCurrency } = this.props;
const {
fromAccount: { balance },
} = this.state;
const balanceInEther = conversionUtil(balance, {
const balanceInBaseAsset = conversionUtil(balance, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
@ -127,7 +94,7 @@ export default class SignatureRequestOriginal extends Component {
{`${this.context.t('balance')}:`}
</div>
<div className="request-signature__balance-value">
{`${balanceInEther} ETH`}
{`${balanceInBaseAsset} ${nativeCurrency}`}
</div>
</div>
);
@ -300,7 +267,6 @@ export default class SignatureRequestOriginal extends Component {
large
className="request-signature__footer__cancel-button"
onClick={async (event) => {
this._removeBeforeUnload();
await cancel(event);
metricsEvent({
eventOpts: {
@ -325,7 +291,6 @@ export default class SignatureRequestOriginal extends Component {
className="request-signature__footer__sign-button"
disabled={hardwareWalletRequiresConnection}
onClick={async (event) => {
this._removeBeforeUnload();
await sign(event);
metricsEvent({
eventOpts: {

@ -13,7 +13,10 @@ import {
import { getAccountByAddress } from '../../../helpers/utils/util';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import {
isAddressLedger,
getNativeCurrency,
} from '../../../ducks/metamask/metamask';
import SignatureRequestOriginal from './signature-request-original.component';
function mapStateToProps(state, ownProps) {
@ -34,6 +37,7 @@ function mapStateToProps(state, ownProps) {
mostRecentOverviewPage: getMostRecentOverviewPage(state),
hardwareWalletRequiresConnection,
isLedgerWallet,
nativeCurrency: getNativeCurrency(state),
// not passed to component
allAccounts: accountsWithSendEtherInfoSelector(state),
domainMetadata: getDomainMetadata(state),

@ -1,12 +1,10 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import Identicon from '../../ui/identicon';
import LedgerInstructionField from '../ledger-instruction-field';
import Header from './signature-request-header';
import Footer from './signature-request-footer';
import Message from './signature-request-message';
import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants';
export default class SignatureRequest extends PureComponent {
static propTypes = {
@ -17,7 +15,6 @@ export default class SignatureRequest extends PureComponent {
name: PropTypes.string,
}).isRequired,
isLedgerWallet: PropTypes.bool,
clearConfirmTransaction: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
sign: PropTypes.func.isRequired,
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
@ -28,33 +25,6 @@ export default class SignatureRequest extends PureComponent {
metricsEvent: PropTypes.func,
};
componentDidMount() {
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload);
}
}
_beforeUnload = (event) => {
const {
clearConfirmTransaction,
cancel,
txData: { type },
} = this.props;
const { metricsEvent } = this.context;
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Cancel Sig Request Via Notification Close',
},
customVariables: {
type,
},
});
clearConfirmTransaction();
cancel(event);
};
formatWallet(wallet) {
return `${wallet.slice(0, 8)}...${wallet.slice(
wallet.length - 8,
@ -79,7 +49,6 @@ export default class SignatureRequest extends PureComponent {
const { metricsEvent } = this.context;
const onSign = (event) => {
window.removeEventListener('beforeunload', this._beforeUnload);
sign(event);
metricsEvent({
eventOpts: {
@ -95,7 +64,6 @@ export default class SignatureRequest extends PureComponent {
};
const onCancel = (event) => {
window.removeEventListener('beforeunload', this._beforeUnload);
cancel(event);
metricsEvent({
eventOpts: {

@ -1,5 +1,4 @@
import { connect } from 'react-redux';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import {
accountsWithSendEtherInfoSelector,
doesAddressRequireLedgerHidConnection,
@ -28,12 +27,6 @@ function mapStateToProps(state, ownProps) {
};
}
function mapDispatchToProps(dispatch) {
return {
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
};
}
function mergeProps(stateProps, dispatchProps, ownProps) {
const {
allAccounts,
@ -83,8 +76,4 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
)(SignatureRequest);
export default connect(mapStateToProps, null, mergeProps)(SignatureRequest);

@ -15,4 +15,55 @@
text-transform: uppercase;
}
}
&-edit-V2 {
margin-bottom: 10px;
display: flex;
align-items: baseline;
justify-content: flex-end;
padding-top: 20px;
button {
@include H7;
display: flex;
align-items: baseline;
color: $primary-1;
background: transparent;
border: 0;
padding-inline-end: 0;
white-space: pre;
}
i {
color: $primary-1;
margin-right: 2px;
}
&__icon {
font-size: 1rem;
}
&__label {
font-size: 12px;
margin-right: 8px;
}
.info-tooltip {
align-self: center;
margin-left: 6px;
}
&__tooltip {
p {
color: $Grey-500;
}
b {
color: $neutral-black;
display: inline-block;
min-width: 60%;
}
}
}
}

@ -2,11 +2,94 @@ import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import { useGasFeeContext } from '../../../contexts/gasFee';
import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
import Typography from '../../ui/typography/typography';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
import { COLORS } from '../../../helpers/constants/design-system';
const GasLevelIconMap = {
low: '🐢',
medium: '🦊',
high: '🦍',
dappSuggested: '🌐',
custom: '⚙',
};
export default function TransactionDetail({ rows = [], onEdit }) {
// eslint-disable-next-line prefer-destructuring
const EIP_1559_V2 = process.env.EIP_1559_V2;
const t = useContext(I18nContext);
const {
estimateToUse,
gasLimit,
gasPrice,
isUsingDappSuggestedGasFees,
maxFeePerGas,
maxPriorityFeePerGas,
transaction,
supportsEIP1559,
} = useGasFeeContext();
const estimateUsed = isUsingDappSuggestedGasFees
? 'dappSuggested'
: estimateToUse;
if (EIP_1559_V2 && estimateUsed) {
return (
<div className="transaction-detail">
<div className="transaction-detail-edit-V2">
<button onClick={onEdit}>
<span className="transaction-detail-edit-V2__icon">
{`${GasLevelIconMap[estimateUsed]} `}
</span>
<span className="transaction-detail-edit-V2__label">
{t(estimateUsed)}
</span>
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
</button>
{estimateUsed === 'custom' && onEdit && (
<button onClick={onEdit}>{t('edit')}</button>
)}
{estimateUsed === 'dappSuggested' && (
<InfoTooltip
contentText={
<div className="transaction-detail-edit-V2__tooltip">
<Typography fontSize="12px" color={COLORS.GREY}>
{t('dappSuggestedTooltip', [transaction.origin])}
</Typography>
{supportsEIP1559 ? (
<>
<Typography fontSize="12px">
<b>{t('maxBaseFee')}</b>
{maxFeePerGas}
</Typography>
<Typography fontSize="12px">
<b>{t('maxPriorityFee')}</b>
{maxPriorityFeePerGas}
</Typography>
</>
) : (
<Typography fontSize="12px">
<b>{t('gasPriceLabel')}</b>
{gasPrice}
</Typography>
)}
<Typography fontSize="12px">
<b>{t('gasLimit')}</b>
{gasLimit}
</Typography>
</div>
}
position="top"
/>
)}
</div>
<div className="transaction-detail-rows">{rows}</div>
</div>
);
}
return (
<div className="transaction-detail">

@ -0,0 +1,94 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { ETH } from '../../../helpers/constants/common';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import TransactionDetail from './transaction-detail.component';
jest.mock('../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
}));
const render = (props) => {
const store = configureStore({
metamask: {
nativeCurrency: ETH,
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
provider: {},
cachedBalances: {},
accounts: {
'0xAddress': {
address: '0xAddress',
balance: '0x176e5b6f173ebe66',
},
},
selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true },
},
});
return renderWithProvider(
<GasFeeContextProvider {...props}>
<TransactionDetail
onEdit={() => {
console.log('on edit');
}}
rows={[]}
{...props}
/>
</GasFeeContextProvider>,
store,
);
};
describe('TransactionDetail', () => {
beforeEach(() => {
process.env.EIP_1559_V2 = true;
});
afterEach(() => {
process.env.EIP_1559_V2 = false;
});
it('should render edit link with text low if low gas estimates are selected', () => {
render({ transaction: { userFeeLevel: 'low' } });
expect(screen.queryByText('🐢')).toBeInTheDocument();
expect(screen.queryByText('Low')).toBeInTheDocument();
});
it('should render edit link with text markey if medium gas estimates are selected', () => {
render({ transaction: { userFeeLevel: 'medium' } });
expect(screen.queryByText('🦊')).toBeInTheDocument();
expect(screen.queryByText('Market')).toBeInTheDocument();
});
it('should render edit link with text agressive if high gas estimates are selected', () => {
render({ transaction: { userFeeLevel: 'high' } });
expect(screen.queryByText('🦍')).toBeInTheDocument();
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
});
it('should render edit link with text Site suggested if site suggested estimated are used', () => {
render({
transaction: {
dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
},
});
expect(screen.queryByText('🌐')).toBeInTheDocument();
expect(screen.queryByText('Site suggested')).toBeInTheDocument();
expect(document.getElementsByClassName('info-tooltip')).toHaveLength(1);
});
it('should render edit link with text advance if custom gas estimates are used', () => {
render({
defaultEstimateToUse: 'custom',
});
expect(screen.queryByText('⚙')).toBeInTheDocument();
expect(screen.queryByText('Advanced')).toBeInTheDocument();
expect(screen.queryByText('Edit')).toBeInTheDocument();
});
});

@ -0,0 +1,42 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import Card from '.';
# Card
Cards are used to group related content or actions together.
<Canvas>
<Story id="ui-components-ui-card-card-stories-js--default-story" />
</Canvas>
## Component API
The `Card` component extends the `Box` component. See the `Box` component for an extended list of props.
<ArgsTable of={Card} />
## Usage
The following describes the props and example usage for this component.
### Padding, Border and Background Color
The Card component has a set of default props that should meet most card use cases. There is a strong recommendation to not overwrite these to ensure our cards stay consistent across the app.
That being said all props can be overwritten if necessary.
```jsx
import { COLORS } from '../../../helpers/constants/design-system';
// To remove the border
<Card border={false} />
// All border related props of the Box component will work
// To remove or change padding
<Card padding={0} />
// All padding related props of the Box component will work
// To change the background color
<Card backgroundColor={COLORS.UI4} />
```

@ -1,23 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
export default class Card extends PureComponent {
static propTypes = {
className: PropTypes.string,
overrideClassName: PropTypes.bool,
title: PropTypes.string,
children: PropTypes.node,
};
render() {
const { className, overrideClassName, title } = this.props;
return (
<div className={classnames({ card: !overrideClassName }, className)}>
<div className="card__title">{title}</div>
{this.props.children}
</div>
);
}
}

@ -1,21 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import Card from './card.component';
describe('Card Component', () => {
it('should render a card with a title and child element', () => {
const wrapper = shallow(
<Card title="Test" className="card-test-class">
<div className="child-test-class">Child</div>
</Card>,
);
expect(wrapper.hasClass('card-test-class')).toStrictEqual(true);
const title = wrapper.find('.card__title');
expect(title).toHaveLength(1);
expect(title.text()).toStrictEqual('Test');
const child = wrapper.find('.child-test-class');
expect(child).toHaveLength(1);
expect(child.text()).toStrictEqual('Child');
});
});

@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import Box from '../box';
import {
BORDER_STYLE,
COLORS,
SIZES,
} from '../../../helpers/constants/design-system';
const Card = ({
border = true,
padding = 4,
backgroundColor = COLORS.WHITE,
children,
...props
}) => {
const defaultBorderProps = {
borderColor: border && COLORS.UI2,
borderRadius: border && SIZES.MD,
borderStyle: border && BORDER_STYLE.SOLID,
};
return (
<Box
{...{
padding,
backgroundColor,
...defaultBorderProps,
...props,
}}
>
{children}
</Box>
);
};
Card.propTypes = {
/**
* Whether the Card has a border or not.
* Defaults to true
*/
border: PropTypes.bool,
/**
* Padding of the Card component accepts number or an array of 2 numbers.
* Defaults to 4 (16px)
*/
padding: Box.propTypes.padding,
/**
* The background color of the card
* Defaults to COLORS.WHITE
*/
backgroundColor: Box.propTypes.backgroundColor,
/**
* The Card component accepts all Box component props
*/
...Box.propTypes,
};
export default Card;

@ -0,0 +1,169 @@
import React from 'react';
import {
ALIGN_ITEMS,
BLOCK_SIZES,
BORDER_STYLE,
COLORS,
DISPLAY,
JUSTIFY_CONTENT,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import README from './README.mdx';
import Card from '.';
const sizeOptions = [undefined, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
export default {
title: 'UI/Card',
id: __filename,
component: Card,
parameters: {
docs: {
page: README,
},
},
argTypes: {
children: { control: 'text' },
border: {
control: 'boolean',
},
borderStyle: {
control: {
type: 'select',
},
options: Object.values(BORDER_STYLE),
},
borderWidth: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
borderColor: {
control: {
type: 'select',
},
options: Object.values(COLORS),
},
backgroundColor: {
control: {
type: 'select',
},
options: Object.values(COLORS),
},
width: {
control: {
type: 'select',
},
options: Object.values(BLOCK_SIZES),
},
height: {
control: {
type: 'select',
},
options: Object.values(BLOCK_SIZES),
},
textAlign: {
control: {
type: 'select',
},
options: Object.values(TEXT_ALIGN),
},
margin: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
marginTop: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
marginRight: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
marginBottom: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
marginLeft: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
padding: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
paddingTop: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
paddingRight: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
paddingBottom: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
paddingLeft: {
control: {
type: 'select',
},
options: [...sizeOptions],
},
display: {
control: {
type: 'select',
},
options: Object.values(DISPLAY),
},
justifyContent: {
control: {
type: 'select',
},
options: Object.values(JUSTIFY_CONTENT),
},
alignItems: {
control: {
type: 'select',
},
options: Object.values(ALIGN_ITEMS),
},
},
args: {
children: 'Card children',
},
};
export const DefaultStory = (args) => <Card {...args}>{args.children}</Card>;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
padding: 4,
border: true,
borderWidth: 1,
borderColor: COLORS.UI2,
borderStyle: BORDER_STYLE.SOLID,
backgroundColor: COLORS.WHITE,
display: DISPLAY.BLOCK,
};

@ -0,0 +1,11 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import Card from '.';
describe('Card', () => {
it('should render the Card without crashing', () => {
const { getByText } = render(<Card>Card content</Card>);
expect(getByText('Card content')).toBeDefined();
});
});

@ -1 +1 @@
export { default } from './card.component';
export { default } from './card';

@ -1,11 +0,0 @@
.card {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
padding: 8px;
&__title {
border-bottom: 1px solid #d8d8d8;
padding-bottom: 4px;
text-transform: capitalize;
}
}

@ -0,0 +1,15 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import Chip from '.';
# Chip
Chips are compact elements that represent an input, status, or action.
<Canvas>
<Story id="ui-components-ui-chip-chip-stories-js--default-story" />
</Canvas>
## Component API
<ArgsTable of={Chip} />

@ -5,6 +5,7 @@ import { COLORS } from '../../../helpers/constants/design-system';
import Chip from '.';
export function ChipWithInput({
dataTestId,
className,
borderColor = COLORS.UI1,
inputValue,
@ -17,6 +18,7 @@ export function ChipWithInput({
>
{setInputValue && (
<input
data-testid={dataTestId}
type="text"
className="chip__input"
onChange={(e) => {
@ -30,6 +32,7 @@ export function ChipWithInput({
}
ChipWithInput.propTypes = {
dataTestId: PropTypes.string,
borderColor: PropTypes.oneOf(Object.values(COLORS)),
className: PropTypes.string,
inputValue: PropTypes.string,

@ -6,9 +6,11 @@ import Typography from '../typography';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
export default function Chip({
dataTestId,
className,
children,
borderColor = COLORS.UI1,
backgroundColor,
label,
labelProps = {},
leftIcon,
@ -25,12 +27,14 @@ export default function Chip({
return (
<div
data-testid={dataTestId}
onClick={onClick}
onKeyPress={onKeyPress}
className={classnames(className, 'chip', {
'chip--with-left-icon': Boolean(leftIcon),
'chip--with-right-icon': Boolean(rightIcon),
[`chip--${borderColor}`]: true,
[`chip--border-color-${borderColor}`]: true,
[`chip--background-color-${backgroundColor}`]: true,
})}
role={isInteractive ? 'button' : undefined}
tabIndex={isInteractive ? 0 : undefined}
@ -53,16 +57,46 @@ export default function Chip({
}
Chip.propTypes = {
/**
* Data test id used for testing of the Chip component
*/
dataTestId: PropTypes.string,
/**
* The border color of the Chip
*/
borderColor: PropTypes.oneOf(Object.values(COLORS)),
/**
* The background color of the Chip component
*/
backgroundColor: PropTypes.oneOf(Object.values(COLORS)),
/**
* The label of the Chip component has a default typography variant of h6 and is a span html element
*/
label: PropTypes.string,
children: PropTypes.node,
/**
* The label props of the component. Most Typography props can be used
*/
labelProps: PropTypes.shape({
...omit(Typography.propTypes, ['children', 'className']),
}),
/**
* Children will replace the label of the Chip component.
*/
children: PropTypes.node,
/**
* An icon component that can be passed to appear on the left of the label
*/
leftIcon: PropTypes.node,
/**
* An icon component that can be passed to appear on the right of the label
*/
rightIcon: PropTypes.node,
/**
* The className of the Chip
*/
className: PropTypes.string,
/**
* The onClick handler to be passed to the Chip component
*/
onClick: PropTypes.func,
inputValue: PropTypes.string,
setInputValue: PropTypes.func,
};

@ -18,9 +18,12 @@
}
@each $variant, $color in design-system.$color-map {
&--#{$variant} {
&--border-color-#{$variant} {
border-color: $color;
}
&--background-color-#{$variant} {
background-color: $color;
}
}
&--with-left-icon,

@ -1,51 +1,111 @@
/* eslint-disable react/prop-types */
import React, { useState } from 'react';
import { select, text } from '@storybook/addon-knobs';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
import ApproveIcon from '../icon/approve-icon.component';
import Identicon from '../identicon/identicon.component';
import { ChipWithInput } from './chip-with-input';
import README from './README.mdx';
import Chip from '.';
export default {
title: 'Chip',
title: 'UI/Chip',
id: __filename,
component: Chip,
parameters: {
docs: {
page: README,
},
},
argTypes: {
leftIcon: {
control: {
type: 'select',
},
options: ['ApproveIcon'],
mapping: {
ApproveIcon: <ApproveIcon size={24} color="#4cd964" />,
},
},
rightIcon: {
control: {
type: 'select',
},
options: ['Identicon'],
mapping: {
Identicon: (
<Identicon
address="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
diameter={25}
/>
),
},
},
label: {
control: 'text',
},
labelProps: {
color: {
control: {
type: 'select',
},
options: Object.values(COLORS),
},
variant: {
color: {
control: {
type: 'select',
},
options: Object.values(TYPOGRAPHY),
},
},
},
borderColor: {
control: {
type: 'select',
},
options: Object.values(COLORS),
},
backgroundColor: {
control: {
type: 'select',
},
options: Object.values(COLORS),
},
children: {
control: 'text',
},
},
};
export const Plain = ({
leftIcon,
rightIcon,
label = 'Hello',
borderColor = COLORS.UI1,
fontColor = COLORS.BLACK,
}) => (
<Chip
leftIcon={leftIcon}
rightIcon={rightIcon}
label={text('label', label)}
labelProps={{
color: select('color', COLORS, fontColor),
variant: select('typography', TYPOGRAPHY, TYPOGRAPHY.H6),
}}
borderColor={select('borderColor', COLORS, borderColor)}
/>
);
export const DefaultStory = (args) => <Chip {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
label: 'Chip',
borderColor: COLORS.UI3,
backgroundColor: COLORS.UI1,
labelProps: {
color: COLORS.BLACK,
variant: TYPOGRAPHY.H6,
},
};
export const WithLeftIcon = () => (
<Plain
<Chip
label="Done!"
borderColor={COLORS.SUCCESS3}
fontColor={COLORS.SUCCESS3}
leftIcon={<ApproveIcon size={24} color="#4cd964" />}
/>
);
export const WithRightIcon = () => (
<Plain
<Chip
label="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
borderColor={COLORS.UI4}
fontColor={COLORS.UI4}
rightIcon={
<Identicon
address="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
@ -56,10 +116,9 @@ export const WithRightIcon = () => (
);
export const WithBothIcons = () => (
<Plain
<Chip
label="Account 1"
borderColor={COLORS.UI4}
fontColor={COLORS.UI4}
rightIcon={
<svg
width="10"
@ -82,13 +141,17 @@ export const WithBothIcons = () => (
}
/>
);
export const WithInput = () => {
const [inputValue, setInputValue] = useState('');
export const WithInput = (args) => {
const [inputValue, setInputValue] = useState('Chip with input');
return (
<ChipWithInput
{...args}
inputValue={inputValue}
setInputValue={setInputValue}
borderColor={select('borderColor', COLORS, COLORS.UI3)}
/>
);
};
WithInput.args = {
borderColor: COLORS.UI3,
};

@ -16,6 +16,7 @@ import NumericInput from '../numeric-input/numeric-input.component';
import InfoTooltip from '../info-tooltip/info-tooltip';
export default function FormField({
dataTestId,
titleText,
titleUnit,
tooltipText,
@ -94,6 +95,7 @@ export default function FormField({
type={password ? 'password' : 'text'}
autoFocus={autoFocus}
disabled={disabled}
data-testid={dataTestId}
/>
)}
{error && (
@ -111,6 +113,7 @@ export default function FormField({
}
FormField.propTypes = {
dataTestId: PropTypes.string,
titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
titleUnit: PropTypes.string,
tooltipText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),

@ -25,10 +25,41 @@
margin-bottom: 9px;
}
&__address-container {
display: flex;
justify-content: center;
&__tooltip-wrapper {
width: 100%;
}
&:hover {
cursor: pointer;
.qr-code__copy-icon__svg {
fill: $primary-1;
}
}
}
&__address {
@include H7;
background-color: $ui-1;
padding: 12px;
background-color: $Grey-000;
width: 76%;
padding: 8px 12px;
word-break: break-all;
text-align: center;
}
&__copy-icon {
height: 13px;
padding: 17px 0;
position: absolute;
right: 24px;
&__svg {
fill: $ui-5;
}
}
}

@ -3,7 +3,11 @@ import React from 'react';
import qrCode from 'qrcode-generator';
import { connect } from 'react-redux';
import { isHexPrefixed } from 'ethereumjs-util';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import Tooltip from '../tooltip';
import CopyIcon from '../icon/copy-icon.component';
import { useI18nContext } from '../../../hooks/useI18nContext';
export default connect(mapStateToProps)(QrCodeView);
@ -22,6 +26,8 @@ function QrCodeView(props) {
const address = `${
isHexPrefixed(data) ? 'ethereum:' : ''
}${toChecksumHexAddress(data)}`;
const [copied, handleCopy] = useCopyToClipboard();
const t = useI18nContext();
const qrImage = qrCode(4, 'M');
qrImage.addData(address);
qrImage.make();
@ -50,7 +56,23 @@ function QrCodeView(props) {
__html: qrImage.createTableTag(4),
}}
/>
<div className="qr-code__address">{toChecksumHexAddress(data)}</div>
<Tooltip
wrapperClassName="qr-code__address-container__tooltip-wrapper"
position="bottom"
title={copied ? t('copiedExclamation') : t('copyToClipboard')}
>
<div
className="qr-code__address-container"
onClick={() => {
handleCopy(toChecksumHexAddress(data));
}}
>
<div className="qr-code__address">{toChecksumHexAddress(data)}</div>
<div className="qr-code__copy-icon">
<CopyIcon size={11} className="qr-code__copy-icon__svg" color="" />
</div>
</div>
</Tooltip>
</div>
);
}

@ -75,11 +75,12 @@ export default class TokenInput extends PureComponent {
return Number(decimalValueString) ? decimalValueString : '';
}
handleChange = (decimalValue) => {
handleChange = (decimalValue, applyDecimals = false) => {
const { token: { decimals } = {}, onChange } = this.props;
let newDecimalValue = decimalValue;
if (decimals) {
if (decimals && applyDecimals) {
newDecimalValue = parseFloat(decimalValue).toFixed(decimals);
}
@ -94,6 +95,10 @@ export default class TokenInput extends PureComponent {
onChange(hexValue);
};
handleBlur = (decimalValue) => {
this.handleChange(decimalValue, true);
};
renderConversionComponent() {
const {
tokenExchangeRates,
@ -155,6 +160,7 @@ export default class TokenInput extends PureComponent {
{...restProps}
suffix={token.symbol}
onChange={this.handleChange}
onBlur={this.handleBlur}
value={decimalValue}
>
{this.renderConversionComponent()}

@ -8,7 +8,6 @@
@import 'button-group/index';
@import 'button/buttons';
@import 'callout/callout';
@import 'card/index';
@import 'check-box/index';
@import 'chip/chip';
@import 'circle-icon/index';

@ -17,6 +17,7 @@ export default class UnitInput extends PureComponent {
actionComponent: PropTypes.node,
error: PropTypes.bool,
onChange: PropTypes.func,
onBlur: PropTypes.func,
placeholder: PropTypes.string,
suffix: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@ -55,6 +56,8 @@ export default class UnitInput extends PureComponent {
if (value === '') {
this.setState({ value: '0' });
}
this.props.onBlur && this.props.onBlur(value);
};
handleChange = (event) => {

@ -0,0 +1,37 @@
import React, { createContext, useContext } from 'react';
import PropTypes from 'prop-types';
import { useGasFeeInputs } from '../hooks/gasFeeInput/useGasFeeInputs';
export const GasFeeContext = createContext({});
export const GasFeeContextProvider = ({
children,
defaultEstimateToUse,
transaction,
minimumGasLimit,
editGasMode,
}) => {
const gasFeeDetails = useGasFeeInputs(
defaultEstimateToUse,
transaction,
minimumGasLimit,
editGasMode,
);
return (
<GasFeeContext.Provider value={gasFeeDetails}>
{children}
</GasFeeContext.Provider>
);
};
export function useGasFeeContext() {
return useContext(GasFeeContext);
}
GasFeeContextProvider.propTypes = {
children: PropTypes.node.isRequired,
defaultEstimateToUse: PropTypes.string,
transaction: PropTypes.object.isRequired,
minimumGasLimit: PropTypes.string,
editGasMode: PropTypes.string,
};

@ -103,6 +103,7 @@ $ui-1: #f2f3f4;
$ui-2: #d6d9dc;
$ui-3: #bbc0c5;
$ui-4: #6a737d;
$ui-5: #c4c4c4;
$mainnet: #29b6af;
$ropsten: #ff4a8d;
@ -116,6 +117,7 @@ $color-map: (
'ui-2': $ui-2,
'ui-3': $ui-3,
'ui-4': $ui-4,
'ui-5': $ui-5,
'white': $ui-white,
'black': $ui-black,
'grey': $ui-grey,

@ -38,6 +38,7 @@ const AWAITING_SIGNATURES_ROUTE = '/swaps/awaiting-signatures';
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap';
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error';
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance';
const ADD_COLLECTIBLE_ROUTE = '/add-collectible';
const INITIALIZE_ROUTE = '/initialize';
const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome';
@ -218,6 +219,7 @@ export {
AWAITING_SIGNATURES_ROUTE,
SWAPS_ERROR_ROUTE,
SWAPS_MAINTENANCE_ROUTE,
ADD_COLLECTIBLE_ROUTE,
ONBOARDING_ROUTE,
ONBOARDING_HELP_US_IMPROVE_ROUTE,
ONBOARDING_CREATE_PASSWORD_ROUTE,

@ -19,3 +19,8 @@ export const TOKEN_CATEGORY_HASH = {
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER]: true,
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM]: true,
};
export const TRANSACTION_ENVELOPE_TYPE_NAMES = {
FEE_MARKET: 'fee-market',
LEGACY: 'legacy',
};

@ -130,7 +130,10 @@ const getMaxFeeWarning = (
return undefined;
};
const getBalanceError = (minimumCostInHexWei, transaction, ethBalance) => {
const hasBalanceError = (minimumCostInHexWei, transaction, ethBalance) => {
if (minimumCostInHexWei === undefined || ethBalance === undefined) {
return false;
}
const minimumTxCostInHexWei = addHexes(
minimumCostInHexWei,
transaction?.txParams?.value || '0x0',
@ -247,7 +250,7 @@ export function useGasFeeErrors({
);
const { balance: ethBalance } = useSelector(getSelectedAccount);
const balanceError = getBalanceError(
const balanceError = hasBalanceError(
minimumCostInHexWei,
transaction,
ethBalance,

@ -1,13 +1,19 @@
import { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { getAdvancedInlineGasShown } from '../../selectors';
import { hexToDecimal } from '../../helpers/utils/conversions.util';
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
import {
GAS_RECOMMENDATIONS,
CUSTOM_GAS_ESTIMATE,
GAS_RECOMMENDATIONS,
EDIT_GAS_MODES,
} from '../../../shared/constants/gas';
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
import { areDappSuggestedAndTxParamGasFeesTheSame } from '../../helpers/utils/confirm-tx.util';
import {
checkNetworkAndAccountSupports1559,
getAdvancedInlineGasShown,
} from '../../selectors';
import { hexToDecimal } from '../../helpers/utils/conversions.util';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
import { useGasFeeErrors } from './useGasFeeErrors';
@ -65,8 +71,12 @@ export function useGasFeeInputs(
defaultEstimateToUse = GAS_RECOMMENDATIONS.MEDIUM,
transaction,
minimumGasLimit = '0x5208',
editGasMode,
editGasMode = EDIT_GAS_MODES.MODIFY_IN_PLACE,
) {
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
// We need the gas estimates from the GasFeeController in the background.
// Calling this hooks initiates polling for new gas estimates and returns the
// current estimate.
@ -90,6 +100,13 @@ export function useGasFeeInputs(
return defaultEstimateToUse;
});
const [
isUsingDappSuggestedGasFees,
setIsUsingDappSuggestedGasFees,
] = useState(() =>
Boolean(areDappSuggestedAndTxParamGasFeesTheSame(transaction)),
);
const [gasLimit, setGasLimit] = useState(
Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')),
);
@ -191,6 +208,7 @@ export function useGasFeeInputs(
setMaxPriorityFeePerGas(null);
setGasPrice(null);
setGasPriceHasBeenManuallySet(false);
setIsUsingDappSuggestedGasFees(false);
},
[
setInternalEstimateToUse,
@ -199,6 +217,7 @@ export function useGasFeeInputs(
setMaxPriorityFeePerGas,
setGasPrice,
setGasPriceHasBeenManuallySet,
setIsUsingDappSuggestedGasFees,
],
);
@ -226,6 +245,7 @@ export function useGasFeeInputs(
]);
return {
transaction,
maxFeePerGas,
maxFeePerGasFiat,
setMaxFeePerGas,
@ -243,6 +263,7 @@ export function useGasFeeInputs(
estimatedMaximumNative,
estimatedMinimumNative,
isGasEstimatesLoading,
isUsingDappSuggestedGasFees,
gasFeeEstimates,
gasEstimateType,
estimatedGasFeeTimeBounds,
@ -254,5 +275,6 @@ export function useGasFeeInputs(
gasErrors,
gasWarnings,
hasGasErrors,
supportsEIP1559,
};
}

@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useI18nContext } from '../../hooks/useI18nContext';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
import Box from '../../components/ui/box';
import TextField from '../../components/ui/text-field';
import PageContainer from '../../components/ui/page-container';
export default function AddCollectible() {
const t = useI18nContext();
const history = useHistory();
const [address, setAddress] = useState('');
const [tokenId, setTokenId] = useState('');
return (
<PageContainer
title={t('addNFT')}
onSubmit={() => {
console.log(
`Adding collectible with ID: ${tokenId} and address ${address}`,
);
history.push(DEFAULT_ROUTE);
}}
submitText={t('add')}
onCancel={() => {
history.push(DEFAULT_ROUTE);
}}
onClose={() => {
history.push(DEFAULT_ROUTE);
}}
disabled={false}
contentComponent={
<Box padding={4}>
<Box>
<TextField
id="address"
label={t('address')}
placeholder="0x..."
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
fullWidth
autoFocus
margin="normal"
/>
</Box>
<Box>
<TextField
id="token-id"
label={t('id')}
placeholder={t('nftTokenIdPlaceholder')}
type="number"
value={tokenId}
onChange={(e) => setTokenId(e.target.value)}
fullWidth
margin="normal"
/>
</Box>
</Box>
}
/>
);
}

@ -0,0 +1 @@
export { default } from './add-collectible.component';

@ -9,9 +9,7 @@ import Identicon from '../../components/ui/identicon';
import Tooltip from '../../components/ui/tooltip';
import Copy from '../../components/ui/icon/copy-icon.component';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
import { SECOND } from '../../../shared/constants/time';
import { getEnvironmentType } from '../../../app/scripts/lib/util';
import { conversionUtil } from '../../../shared/modules/conversion.utils';
export default class ConfirmDecryptMessage extends Component {
@ -44,44 +42,6 @@ export default class ConfirmDecryptMessage extends Component {
hasCopied: false,
};
componentDidMount = () => {
if (
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
) {
window.addEventListener('beforeunload', this._beforeUnload);
}
};
componentWillUnmount = () => {
this._removeBeforeUnload();
};
_beforeUnload = async (event) => {
const {
clearConfirmTransaction,
cancelDecryptMessage,
txData,
} = this.props;
const { metricsEvent } = this.context;
await cancelDecryptMessage(txData, event);
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Request',
name: 'Cancel Via Notification Close',
},
});
clearConfirmTransaction();
};
_removeBeforeUnload = () => {
if (
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
) {
window.removeEventListener('beforeunload', this._beforeUnload);
}
};
copyMessage = () => {
copyToClipboard(this.state.rawMessage);
this.context.metricsEvent({

@ -5,8 +5,6 @@ import AccountListItem from '../../components/app/account-list-item';
import Button from '../../components/ui/button';
import Identicon from '../../components/ui/identicon';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
import { getEnvironmentType } from '../../../app/scripts/lib/util';
import { conversionUtil } from '../../../shared/modules/conversion.utils';
export default class ConfirmEncryptionPublicKey extends Component {
@ -33,44 +31,6 @@ export default class ConfirmEncryptionPublicKey extends Component {
nativeCurrency: PropTypes.string.isRequired,
};
componentDidMount = () => {
if (
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
) {
window.addEventListener('beforeunload', this._beforeUnload);
}
};
componentWillUnmount = () => {
this._removeBeforeUnload();
};
_beforeUnload = async (event) => {
const {
clearConfirmTransaction,
cancelEncryptionPublicKey,
txData,
} = this.props;
const { metricsEvent } = this.context;
await cancelEncryptionPublicKey(txData, event);
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Encryption public key Request',
name: 'Cancel Via Notification Close',
},
});
clearConfirmTransaction();
};
_removeBeforeUnload = () => {
if (
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
) {
window.removeEventListener('beforeunload', this._beforeUnload);
}
};
renderHeader = () => {
return (
<div className="request-encryption-public-key__header">

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save