mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge pull request #16485 from MetaMask/Version-v10.23.0
Version v10.23.0
This commit is contained in:
commit
6af3f9a4fb
@ -323,9 +323,25 @@ jobs:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: build:dist
|
||||
command: yarn build --build-type flask dist
|
||||
- when:
|
||||
condition:
|
||||
not:
|
||||
matches:
|
||||
pattern: /^master$/
|
||||
value: << pipeline.git.branch >>
|
||||
steps:
|
||||
- run:
|
||||
name: build:dist
|
||||
command: yarn build --build-type flask dist
|
||||
- when:
|
||||
condition:
|
||||
matches:
|
||||
pattern: /^master$/
|
||||
value: << pipeline.git.branch >>
|
||||
steps:
|
||||
- run:
|
||||
name: build:prod
|
||||
command: yarn build --build-type flask prod
|
||||
- run:
|
||||
name: build:debug
|
||||
command: find dist/ -type f -exec md5sum {} \; | sort -k 2
|
||||
@ -532,6 +548,7 @@ jobs:
|
||||
|
||||
test-e2e-chrome:
|
||||
executor: node-browsers
|
||||
parallelism: 8
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -556,9 +573,10 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: test-artifacts
|
||||
destination: test-artifacts
|
||||
|
||||
|
||||
test-e2e-chrome-mv3:
|
||||
executor: node-browsers
|
||||
parallelism: 8
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -586,6 +604,7 @@ jobs:
|
||||
|
||||
test-e2e-firefox-snaps:
|
||||
executor: node-browsers
|
||||
parallelism: 2
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -613,6 +632,7 @@ jobs:
|
||||
|
||||
test-e2e-chrome-snaps:
|
||||
executor: node-browsers
|
||||
parallelism: 2
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -640,6 +660,7 @@ jobs:
|
||||
|
||||
test-e2e-firefox:
|
||||
executor: node-browsers-medium-plus
|
||||
parallelism: 8
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -808,6 +829,11 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: development/ts-migration-dashboard/build
|
||||
destination: ts-migration-dashboard
|
||||
- run:
|
||||
name: Set branch parent commit env var
|
||||
command: |
|
||||
echo "export PARENT_COMMIT=$(git rev-parse "$(git rev-list --topo-order --reverse HEAD ^origin/develop | head -1)"^)" >> $BASH_ENV
|
||||
source $BASH_ENV
|
||||
- run:
|
||||
name: build:announce
|
||||
command: ./development/metamaskbot-build-announce.js
|
||||
|
@ -4,7 +4,7 @@ set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
FIREFOX_VERSION='102.0'
|
||||
FIREFOX_VERSION='106.0.4'
|
||||
FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2"
|
||||
FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}"
|
||||
FIREFOX_PATH='/opt/firefox'
|
||||
|
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -6,18 +6,6 @@ Thanks for the pull request. Take a moment to answer these questions so that rev
|
||||
* What is the current state of things and why does it need to change?
|
||||
* What is the solution your changes offer and how does it work?
|
||||
|
||||
Below is a template to give you some ideas. Feel free to use your own words!
|
||||
|
||||
Currently, ...
|
||||
|
||||
This is a problem because ...
|
||||
|
||||
In order to solve this problem, this pull request ...
|
||||
-->
|
||||
|
||||
## More Information
|
||||
|
||||
<!--
|
||||
Are there any issues, Slack conversations, Zendesk issues, user stories, etc. reviewers should consult to understand this pull request better? For instance:
|
||||
|
||||
* Fixes #12345
|
||||
@ -46,14 +34,20 @@ How should reviewers and QA manually test your changes? For instance:
|
||||
- Then do this
|
||||
-->
|
||||
|
||||
## Pre-Merge Checklist
|
||||
## Pre-merge author checklist
|
||||
|
||||
- [ ] PR template is filled out
|
||||
- [ ] **IF** this PR fixes a bug, a test that _would have_ caught the bug has been added
|
||||
- [ ] I've clearly explained:
|
||||
- [ ] What problem this PR is solving
|
||||
- [ ] How this problem was solved
|
||||
- [ ] How reviewers can test my changes
|
||||
- [ ] Sufficient automated test coverage has been added
|
||||
|
||||
## Pre-merge reviewer checklist
|
||||
|
||||
- [ ] Manual testing (e.g. pull and build branch, run in browser, test code being changed)
|
||||
- [ ] PR is linked to the appropriate GitHub issue
|
||||
- [ ] PR has been added to the appropriate release Milestone
|
||||
- [ ] **IF** this PR fixes a bug in the release milestone, add this PR to the release milestone
|
||||
|
||||
### + If there are functional changes:
|
||||
If further QA is required (e.g. new feature, complex testing steps, large refactor), add the `Extension QA Board` label.
|
||||
|
||||
- [ ] Manual testing complete & passed
|
||||
- [ ] "Extension QA Board" label has been applied
|
||||
In this case, a QA Engineer approval will be be required.
|
||||
|
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
CLABot:
|
||||
if: github.event_name == 'pull_request_target' || contains(github.event.comment.html_url, '/pull/')
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
2
.github/workflows/crowdin_action.yml
vendored
2
.github/workflows/crowdin_action.yml
vendored
@ -13,7 +13,7 @@ on:
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [10.23.0]
|
||||
### Added
|
||||
- Add Picker Network Component ([#16340](https://github.com/MetaMask/metamask-extension/pull/16340))
|
||||
- Add Button component, unifying primary, secondary and link buttons([#16305](https://github.com/MetaMask/metamask-extension/pull/16305))
|
||||
- Add Button Icon component ([#16277](https://github.com/MetaMask/metamask-extension/pull/16277))
|
||||
- Swaps: enable Swaps functionality on Arbitrum and Optimism networks ([#16396](https://github.com/MetaMask/metamask-extension/pull/16396))
|
||||
- [FLASK] Add snap cronjobs ([#16239](https://github.com/MetaMask/metamask-extension/pull/16239))
|
||||
|
||||
### Changed
|
||||
- Replace every Address value by the Address component on SignTypedData v4 Signature screen ([#16018](https://github.com/MetaMask/metamask-extension/pull/16018))
|
||||
- Update Address component on Transaction data screen by displaying Account name, Contact name, or Contract name when corresponds ([#15888](https://github.com/MetaMask/metamask-extension/pull/15888))
|
||||
- Bump `@metamask/providers` from `10.0.0` to `10.2.0` ([#16361](https://github.com/MetaMask/metamask-extension/pull/16361))
|
||||
- [FLASK] **BREAKING**: Snaps no longer automatically receive a `Buffer` polyfill ([#16394](https://github.com/MetaMask/metamask-extension/pull/16394))
|
||||
- To work around this you can either use typed arrays or include a polyfill yourself.
|
||||
- [FLASK] **BREAKING**: Snap RPC methods now use `@metamask/key-tree@6.0.0` ([#16394](https://github.com/MetaMask/metamask-extension/pull/16394))
|
||||
- In the new version, all hexadecimal values are prefixed with `0x`
|
||||
- All fields containing the word `Buffer` has also been renamed to `Bytes`
|
||||
- Please update your snap to use the latest version
|
||||
|
||||
### Fixed
|
||||
- Fix Settings Search pointing into the incorrect row for Token Detection entry ([#16407](https://github.com/MetaMask/metamask-extension/pull/16407))
|
||||
- Fix Balance not updating when using a duplicated `chainId` network ([#14245](https://github.com/MetaMask/metamask-extension/pull/14245))
|
||||
- [FLASK] Fix an issue with updating snaps that require certain permissions ([#16473](https://github.com/MetaMask/metamask-extension/pull/16473))
|
||||
- [FLASK] Fix some issues with installing snaps that request `eth_accounts` ([#16365](https://github.com/MetaMask/metamask-extension/pull/16365))
|
||||
- [FLASK] Catch and display errors in snaps insight ([#16416](https://github.com/MetaMask/metamask-extension/pull/16416))
|
||||
|
||||
## [10.22.3]
|
||||
### Added
|
||||
- [Beta]: Add Beta banner to all screens ([#16307](https://github.com/MetaMask/metamask-extension/pull/16307))
|
||||
@ -30,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
- Add Aurora network to the Popular Custom Network list ([#16039](https://github.com/MetaMask/metamask-extension/pull/16039))
|
||||
- Add array of valid sizes for Box `height` and `width` to support responsive layout ([#16111](https://github.com/MetaMask/metamask-extension/pull/16111))
|
||||
- Add Warning on a Send transaction request when user doesn't have funds ([#16220](https://github.com/MetaMask/metamask-extension/pull/16220))
|
||||
- [FLASK] Allow snaps insights to show on regular EOA transactions ([#16093](https://github.com/MetaMask/metamask-extension/pull/16093))
|
||||
|
||||
### Changed
|
||||
@ -3309,7 +3336,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Uncategorized
|
||||
- Added the ability to restore accounts from seed words.
|
||||
|
||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.22.3...HEAD
|
||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.23.0...HEAD
|
||||
[10.23.0]: https://github.com/MetaMask/metamask-extension/compare/v10.22.3...v10.23.0
|
||||
[10.22.3]: https://github.com/MetaMask/metamask-extension/compare/v10.22.2...v10.22.3
|
||||
[10.22.2]: https://github.com/MetaMask/metamask-extension/compare/v10.22.1...v10.22.2
|
||||
[10.22.1]: https://github.com/MetaMask/metamask-extension/compare/v10.22.0...v10.22.1
|
||||
|
13
README.md
13
README.md
@ -15,7 +15,7 @@ To learn how to contribute to the MetaMask project itself, visit our [Internal D
|
||||
## Building locally
|
||||
|
||||
- Install [Node.js](https://nodejs.org) version 16
|
||||
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
|
||||
- If you are using [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) (recommended) running `nvm use` will automatically choose the right node version for you.
|
||||
- Install [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
- Install dependencies: `yarn setup` (not the usual install command)
|
||||
- Copy the `.metamaskrc.dist` file to `.metamaskrc`
|
||||
@ -61,13 +61,16 @@ You can run the linter by itself with `yarn lint`, and you can automatically fix
|
||||
|
||||
### Running E2E Tests
|
||||
|
||||
Our e2e test suite can be run on either Firefox or Chrome. In either case, start by creating a test build by running `yarn build:test`.
|
||||
Our e2e test suite can be run on either Firefox or Chrome.
|
||||
|
||||
- Firefox e2e tests can be run with `yarn test:e2e:firefox`.
|
||||
1. **required** `yarn build:test` to create a test build.
|
||||
2. run tests, targetting the browser:
|
||||
* Firefox e2e tests can be run with `yarn test:e2e:firefox`.
|
||||
* Chrome e2e tests can be run with `yarn test:e2e:chrome`. The `chromedriver` package major version must match the major version of your local Chrome installation. If they don't match, update whichever is behind before running Chrome e2e tests.
|
||||
|
||||
- Chrome e2e tests can be run with `yarn test:e2e:chrome`. The `chromedriver` package major version must match the major version of your local Chrome installation. If they don't match, update whichever is behind before running Chrome e2e tests.
|
||||
#### Running a single e2e test
|
||||
|
||||
- Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below.
|
||||
Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below.
|
||||
|
||||
```console
|
||||
--browser Set the browser used; either 'chrome' or 'firefox'.
|
||||
|
24
app/_locales/en/messages.json
generated
24
app/_locales/en/messages.json
generated
@ -2153,6 +2153,9 @@
|
||||
"networkName": {
|
||||
"message": "Network name"
|
||||
},
|
||||
"networkNameArbitrum": {
|
||||
"message": "Arbitrum"
|
||||
},
|
||||
"networkNameAvalanche": {
|
||||
"message": "Avalanche"
|
||||
},
|
||||
@ -2168,6 +2171,9 @@
|
||||
"networkNameGoerli": {
|
||||
"message": "Goerli"
|
||||
},
|
||||
"networkNameOptimism": {
|
||||
"message": "Optimism"
|
||||
},
|
||||
"networkNamePolygon": {
|
||||
"message": "Polygon"
|
||||
},
|
||||
@ -2664,6 +2670,10 @@
|
||||
"message": "Connect to the $1 Snap.",
|
||||
"description": "The description for the `wallet_snap_*` permission. $1 is the name of the Snap."
|
||||
},
|
||||
"permission_cronjob": {
|
||||
"message": "Schedule and execute periodic actions.",
|
||||
"description": "The description for the `snap_cronjob` permission"
|
||||
},
|
||||
"permission_customConfirmation": {
|
||||
"message": "Display a confirmation in MetaMask.",
|
||||
"description": "The description for the `snap_confirm` permission"
|
||||
@ -2947,6 +2957,10 @@
|
||||
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
|
||||
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
|
||||
},
|
||||
"revokeSpendingCap": {
|
||||
"message": "Revoke spending cap for your $1",
|
||||
"description": "$1 is a token symbol"
|
||||
},
|
||||
"revokeSpendingCapTooltipText": {
|
||||
"message": "This contract will be unable to spend any more of your current or future tokens."
|
||||
},
|
||||
@ -3135,7 +3149,8 @@
|
||||
"description": "The token symbol that is being approved"
|
||||
},
|
||||
"setSpendingCap": {
|
||||
"message": "Set a spending cap for your"
|
||||
"message": "Set a spending cap for your $1",
|
||||
"description": "$1 is a token symbol"
|
||||
},
|
||||
"settings": {
|
||||
"message": "Settings"
|
||||
@ -3270,6 +3285,10 @@
|
||||
"snaps": {
|
||||
"message": "Snaps"
|
||||
},
|
||||
"snapsInsightError": {
|
||||
"message": "An error occured with $1: $2",
|
||||
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
|
||||
},
|
||||
"snapsInsightLoading": {
|
||||
"message": "Loading transaction insight..."
|
||||
},
|
||||
@ -3288,6 +3307,9 @@
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network."
|
||||
},
|
||||
"somethingIsWrong": {
|
||||
"message": "Something's gone wrong. Try reloading the page."
|
||||
},
|
||||
"somethingWentWrong": {
|
||||
"message": "Oops! Something went wrong."
|
||||
},
|
||||
|
@ -79,7 +79,7 @@ const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
|
||||
let versionedData;
|
||||
|
||||
if (inTest || process.env.METAMASK_DEBUG) {
|
||||
global.metamaskGetState = localStore.get.bind(localStore);
|
||||
global.stateHooks.metamaskGetState = localStore.get.bind(localStore);
|
||||
}
|
||||
|
||||
const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
|
||||
@ -88,6 +88,9 @@ const ONE_SECOND_IN_MILLISECONDS = 1_000;
|
||||
// Timeout for initializing phishing warning page.
|
||||
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
|
||||
|
||||
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
|
||||
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
|
||||
|
||||
/**
|
||||
* In case of MV3 we attach a "onConnect" event listener as soon as the application is initialised.
|
||||
* Reason is that in case of MV3 a delay in doing this was resulting in missing first connect event after service worker is re-activated.
|
||||
@ -439,6 +442,14 @@ function setupController(initState, initLangCode, remoteSourcePort) {
|
||||
// This ensures that UI is initialised only after background is ready
|
||||
// It fixes the issue of blank screen coming when extension is loaded, the issue is very frequent in MV3
|
||||
remotePort.postMessage({ name: 'CONNECTION_READY' });
|
||||
|
||||
// If we get a WORKER_KEEP_ALIVE message, we respond with an ACK
|
||||
remotePort.onMessage.addListener((message) => {
|
||||
if (message.name === WORKER_KEEP_ALIVE_MESSAGE) {
|
||||
// To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI.
|
||||
remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (processName === ENVIRONMENT_TYPE_POPUP) {
|
||||
@ -742,7 +753,7 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
|
||||
});
|
||||
|
||||
function setupSentryGetStateGlobal(store) {
|
||||
global.sentryHooks.getSentryState = function () {
|
||||
global.stateHooks.getSentryState = function () {
|
||||
const fullState = store.getState();
|
||||
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE);
|
||||
return {
|
||||
|
@ -193,11 +193,9 @@ export default class AppStateController extends EventEmitter {
|
||||
const { timeoutMinutes } = this.store.getState();
|
||||
|
||||
if (this.timer) {
|
||||
if (isManifestV3) {
|
||||
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
|
||||
} else {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
} else if (isManifestV3) {
|
||||
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
|
||||
}
|
||||
|
||||
if (!timeoutMinutes) {
|
||||
@ -209,16 +207,11 @@ export default class AppStateController extends EventEmitter {
|
||||
delayInMinutes: timeoutMinutes,
|
||||
periodInMinutes: timeoutMinutes,
|
||||
});
|
||||
chrome.alarms.onAlarm.addListener(() => {
|
||||
chrome.alarms.getAll((alarms) => {
|
||||
const hasAlarm = alarms.find(
|
||||
(alarm) => alarm.name === AUTO_LOCK_TIMEOUT_ALARM,
|
||||
);
|
||||
if (hasAlarm) {
|
||||
this.onInactiveTimeout();
|
||||
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
|
||||
}
|
||||
});
|
||||
chrome.alarms.onAlarm.addListener((alarmInfo) => {
|
||||
if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) {
|
||||
this.onInactiveTimeout();
|
||||
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.timer = setTimeout(
|
||||
|
@ -3,12 +3,12 @@ import sinon from 'sinon';
|
||||
import nock from 'nock';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ControllerMessenger } from '@metamask/controllers';
|
||||
import {
|
||||
ControllerMessenger,
|
||||
TokenListController,
|
||||
TokensController,
|
||||
AssetsContractController,
|
||||
} from '@metamask/controllers';
|
||||
} from '@metamask/assets-controllers';
|
||||
import { NETWORK_TYPES } from '../../../shared/constants/network';
|
||||
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
|
||||
import DetectTokensController from './detect-tokens';
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import { isManifestV3 } from '../../../shared/modules/mv3.utils';
|
||||
import { METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM } from '../../../shared/constants/alarms';
|
||||
import { checkAlarmExists } from '../lib/util';
|
||||
import { checkAlarmExists, generateRandomId, isValidDate } from '../lib/util';
|
||||
|
||||
const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled';
|
||||
|
||||
@ -32,6 +32,22 @@ const defaultCaptureException = (err) => {
|
||||
});
|
||||
};
|
||||
|
||||
// The function is used to build a unique messageId for segment messages
|
||||
// It uses actionId and uniqueIdentifier from event if present
|
||||
const buildUniqueMessageId = (args) => {
|
||||
let messageId = '';
|
||||
if (args.uniqueIdentifier) {
|
||||
messageId += `${args.uniqueIdentifier}-`;
|
||||
}
|
||||
if (args.actionId) {
|
||||
messageId += args.actionId;
|
||||
}
|
||||
if (messageId.length) {
|
||||
return messageId;
|
||||
}
|
||||
return generateRandomId();
|
||||
};
|
||||
|
||||
const exceptionsToFilter = {
|
||||
[`You must pass either an "anonymousId" or a "userId".`]: true,
|
||||
};
|
||||
@ -60,6 +76,8 @@ const exceptionsToFilter = {
|
||||
* @property {Array} [eventsBeforeMetricsOptIn] - Array of queued events added before
|
||||
* a user opts into metrics.
|
||||
* @property {object} [traits] - Traits that are not derived from other state keys.
|
||||
* @property {Record<string any>} [previousUserTraits] - The user traits the last
|
||||
* time they were computed.
|
||||
*/
|
||||
|
||||
export default class MetaMetricsController {
|
||||
@ -110,6 +128,7 @@ export default class MetaMetricsController {
|
||||
this.environment = environment;
|
||||
|
||||
const abandonedFragments = omitBy(initState?.fragments, 'persist');
|
||||
const segmentApiCalls = initState?.segmentApiCalls || {};
|
||||
|
||||
this.store = new ObservableStore({
|
||||
participateInMetaMetrics: null,
|
||||
@ -120,6 +139,9 @@ export default class MetaMetricsController {
|
||||
fragments: {
|
||||
...initState?.fragments,
|
||||
},
|
||||
segmentApiCalls: {
|
||||
...segmentApiCalls,
|
||||
},
|
||||
});
|
||||
|
||||
preferencesStore.subscribe(({ currentLocale }) => {
|
||||
@ -142,6 +164,13 @@ export default class MetaMetricsController {
|
||||
this.finalizeEventFragment(fragment.id, { abandoned: true });
|
||||
});
|
||||
|
||||
// Code below submits any pending segmentApiCalls to Segment if/when the controller is re-instantiated
|
||||
if (isManifestV3) {
|
||||
Object.values(segmentApiCalls).forEach(({ eventType, payload }) => {
|
||||
this._submitSegmentAPICall(eventType, payload);
|
||||
});
|
||||
}
|
||||
|
||||
// Close out event fragments that were created but not progressed. An
|
||||
// interval is used to routinely check if a fragment has not been updated
|
||||
// within the fragment's timeout window. When creating a new event fragment
|
||||
@ -162,17 +191,10 @@ export default class MetaMetricsController {
|
||||
});
|
||||
}
|
||||
});
|
||||
chrome.alarms.onAlarm.addListener(() => {
|
||||
chrome.alarms.getAll((alarms) => {
|
||||
const hasAlarm = checkAlarmExists(
|
||||
alarms,
|
||||
METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM,
|
||||
);
|
||||
|
||||
if (hasAlarm) {
|
||||
this.finalizeAbandonedFragments();
|
||||
}
|
||||
});
|
||||
chrome.alarms.onAlarm.addListener((alarmInfo) => {
|
||||
if (alarmInfo.name === METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM) {
|
||||
this.finalizeAbandonedFragments();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setInterval(() => {
|
||||
@ -225,14 +247,6 @@ export default class MetaMetricsController {
|
||||
);
|
||||
}
|
||||
|
||||
const existingFragment = this.getExistingEventFragment(
|
||||
options.actionId,
|
||||
options.uniqueIdentifier,
|
||||
);
|
||||
if (existingFragment) {
|
||||
return existingFragment;
|
||||
}
|
||||
|
||||
const { fragments } = this.store.getState();
|
||||
|
||||
const id = options.uniqueIdentifier ?? uuidv4();
|
||||
@ -260,6 +274,8 @@ export default class MetaMetricsController {
|
||||
value: fragment.value,
|
||||
currency: fragment.currency,
|
||||
environmentType: fragment.environmentType,
|
||||
actionId: options.actionId,
|
||||
uniqueIdentifier: options.uniqueIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
@ -281,26 +297,6 @@ export default class MetaMetricsController {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fragment stored in memory with provided id or undefined if it
|
||||
* does not exist.
|
||||
*
|
||||
* @param {string} actionId - actionId passed from UI
|
||||
* @param {string} uniqueIdentifier - uniqueIdentifier of the event
|
||||
* @returns {[MetaMetricsEventFragment]}
|
||||
*/
|
||||
getExistingEventFragment(actionId, uniqueIdentifier) {
|
||||
const { fragments } = this.store.getState();
|
||||
|
||||
const existingFragment = Object.values(fragments).find(
|
||||
(fragment) =>
|
||||
fragment.actionId === actionId &&
|
||||
fragment.uniqueIdentifier === uniqueIdentifier,
|
||||
);
|
||||
|
||||
return existingFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an event fragment in state
|
||||
*
|
||||
@ -361,6 +357,8 @@ export default class MetaMetricsController {
|
||||
value: fragment.value,
|
||||
currency: fragment.currency,
|
||||
environmentType: fragment.environmentType,
|
||||
actionId: fragment.actionId,
|
||||
uniqueIdentifier: fragment.uniqueIdentifier,
|
||||
});
|
||||
const { fragments } = this.store.getState();
|
||||
delete fragments[id];
|
||||
@ -447,7 +445,10 @@ export default class MetaMetricsController {
|
||||
* @param {MetaMetricsPageOptions} [options] - options for handling the page
|
||||
* view
|
||||
*/
|
||||
trackPage({ name, params, environmentType, page, referrer }, options) {
|
||||
trackPage(
|
||||
{ name, params, environmentType, page, referrer, actionId },
|
||||
options,
|
||||
) {
|
||||
try {
|
||||
if (this.state.participateInMetaMetrics === false) {
|
||||
return;
|
||||
@ -462,7 +463,8 @@ export default class MetaMetricsController {
|
||||
const { metaMetricsId } = this.state;
|
||||
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
|
||||
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
|
||||
this.segment.page({
|
||||
this._submitSegmentAPICall('page', {
|
||||
messageId: buildUniqueMessageId({ actionId }),
|
||||
[idTrait]: idValue,
|
||||
name,
|
||||
properties: {
|
||||
@ -653,6 +655,7 @@ export default class MetaMetricsController {
|
||||
} = rawPayload;
|
||||
return {
|
||||
event,
|
||||
messageId: buildUniqueMessageId(rawPayload),
|
||||
properties: {
|
||||
// These values are omitted from properties because they have special meaning
|
||||
// in segment. https://segment.com/docs/connections/spec/track/#properties.
|
||||
@ -682,7 +685,7 @@ export default class MetaMetricsController {
|
||||
* @returns {MetaMetricsTraits | null} traits that have changed since last update
|
||||
*/
|
||||
_buildUserTraitsObject(metamaskState) {
|
||||
const { traits } = this.store.getState();
|
||||
const { traits, previousUserTraits } = this.store.getState();
|
||||
/** @type {MetaMetricsTraits} */
|
||||
const currentTraits = {
|
||||
[TRAITS.ADDRESS_BOOK_ENTRIES]: sum(
|
||||
@ -719,17 +722,17 @@ export default class MetaMetricsController {
|
||||
[TRAITS.TOKEN_DETECTION_ENABLED]: metamaskState.useTokenDetection,
|
||||
};
|
||||
|
||||
if (!this.previousTraits) {
|
||||
this.previousTraits = currentTraits;
|
||||
if (!previousUserTraits) {
|
||||
this.store.updateState({ previousUserTraits: currentTraits });
|
||||
return currentTraits;
|
||||
}
|
||||
|
||||
if (this.previousTraits && !isEqual(this.previousTraits, currentTraits)) {
|
||||
if (previousUserTraits && !isEqual(previousUserTraits, currentTraits)) {
|
||||
const updates = pickBy(
|
||||
currentTraits,
|
||||
(v, k) => !isEqual(this.previousTraits[k], v),
|
||||
(v, k) => !isEqual(previousUserTraits[k], v),
|
||||
);
|
||||
this.previousTraits = currentTraits;
|
||||
this.store.updateState({ previousUserTraits: currentTraits });
|
||||
return updates;
|
||||
}
|
||||
|
||||
@ -815,7 +818,7 @@ export default class MetaMetricsController {
|
||||
}
|
||||
|
||||
try {
|
||||
this.segment.identify({
|
||||
this._submitSegmentAPICall('identify', {
|
||||
userId: metaMetricsId,
|
||||
traits: userTraits,
|
||||
});
|
||||
@ -944,10 +947,53 @@ export default class MetaMetricsController {
|
||||
return resolve();
|
||||
};
|
||||
|
||||
this.segment.track(payload, callback);
|
||||
this._submitSegmentAPICall('track', payload, callback);
|
||||
if (flushImmediately) {
|
||||
this.segment.flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Method below submits the request to analytics SDK.
|
||||
// It will also add event to controller store
|
||||
// and pass a callback to remove it from store once request is submitted to segment
|
||||
// Saving segmentApiCalls in controller store in MV3 ensures that events are tracked
|
||||
// even if service worker terminates before events are submiteed to segment.
|
||||
_submitSegmentAPICall(eventType, payload, callback) {
|
||||
const { metaMetricsId, participateInMetaMetrics } = this.state;
|
||||
if (!participateInMetaMetrics || !metaMetricsId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = payload.messageId || generateRandomId();
|
||||
let timestamp = new Date();
|
||||
if (payload.timestamp) {
|
||||
const payloadDate = new Date(payload.timestamp);
|
||||
if (isValidDate(payloadDate)) {
|
||||
timestamp = payloadDate;
|
||||
}
|
||||
}
|
||||
const modifiedPayload = { ...payload, messageId, timestamp };
|
||||
this.store.updateState({
|
||||
segmentApiCalls: {
|
||||
...this.store.getState().segmentApiCalls,
|
||||
[messageId]: {
|
||||
eventType,
|
||||
payload: {
|
||||
...modifiedPayload,
|
||||
timestamp: modifiedPayload.timestamp.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const modifiedCallback = (result) => {
|
||||
const { segmentApiCalls } = this.store.getState();
|
||||
delete segmentApiCalls[messageId];
|
||||
this.store.updateState({
|
||||
segmentApiCalls,
|
||||
});
|
||||
return callback?.(result);
|
||||
};
|
||||
this.segment[eventType](modifiedPayload, modifiedCallback);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import waitUntilCalled from '../../../test/lib/wait-until-called';
|
||||
import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network';
|
||||
import * as Utils from '../lib/util';
|
||||
import MetaMetricsController from './metametrics';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
@ -19,6 +20,7 @@ const NETWORK = 'Mainnet';
|
||||
const FAKE_CHAIN_ID = '0x1338';
|
||||
const LOCALE = 'en_US';
|
||||
const TEST_META_METRICS_ID = '0xabc';
|
||||
const DUMMY_ACTION_ID = 'DUMMY_ACTION_ID';
|
||||
|
||||
const MOCK_TRAITS = {
|
||||
test_boolean: true,
|
||||
@ -124,9 +126,10 @@ function getMetaMetricsController({
|
||||
metaMetricsId = TEST_META_METRICS_ID,
|
||||
preferencesStore = getMockPreferencesStore(),
|
||||
networkController = getMockNetworkController(),
|
||||
segmentInstance,
|
||||
} = {}) {
|
||||
return new MetaMetricsController({
|
||||
segment,
|
||||
segment: segmentInstance || segment,
|
||||
getNetworkIdentifier:
|
||||
networkController.getNetworkIdentifier.bind(networkController),
|
||||
getCurrentChainId:
|
||||
@ -145,10 +148,17 @@ function getMetaMetricsController({
|
||||
testid: SAMPLE_PERSISTED_EVENT,
|
||||
testid2: SAMPLE_NON_PERSISTED_EVENT,
|
||||
},
|
||||
events: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
describe('MetaMetricsController', function () {
|
||||
const now = new Date();
|
||||
let clock;
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers(now.getTime());
|
||||
sinon.stub(Utils, 'generateRandomId').returns('DUMMY_RANDOM_ID');
|
||||
});
|
||||
describe('constructor', function () {
|
||||
it('should properly initialize', function () {
|
||||
const mock = sinon.mock(segment);
|
||||
@ -163,6 +173,8 @@ describe('MetaMetricsController', function () {
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
test: true,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
assert.strictEqual(metaMetricsController.version, VERSION);
|
||||
@ -233,15 +245,18 @@ describe('MetaMetricsController', function () {
|
||||
});
|
||||
const mock = sinon.mock(segment);
|
||||
|
||||
mock
|
||||
.expects('identify')
|
||||
.once()
|
||||
.withArgs({ userId: TEST_META_METRICS_ID, traits: MOCK_TRAITS });
|
||||
mock.expects('identify').once().withArgs({
|
||||
userId: TEST_META_METRICS_ID,
|
||||
traits: MOCK_TRAITS,
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
metaMetricsController.identify({
|
||||
...MOCK_TRAITS,
|
||||
...MOCK_INVALID_TRAITS,
|
||||
});
|
||||
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
@ -263,6 +278,8 @@ describe('MetaMetricsController', function () {
|
||||
traits: {
|
||||
test_date: mockDateISOString,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
metaMetricsController.identify({
|
||||
@ -345,7 +362,7 @@ describe('MetaMetricsController', function () {
|
||||
it('should track an event if user has not opted in, but isOptIn is true', function () {
|
||||
const mock = sinon.mock(segment);
|
||||
const metaMetricsController = getMetaMetricsController({
|
||||
participateInMetaMetrics: false,
|
||||
participateInMetaMetrics: true,
|
||||
});
|
||||
mock
|
||||
.expects('track')
|
||||
@ -358,6 +375,8 @@ describe('MetaMetricsController', function () {
|
||||
test: 1,
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
@ -375,7 +394,7 @@ describe('MetaMetricsController', function () {
|
||||
it('should track an event during optin and allow for metaMetricsId override', function () {
|
||||
const mock = sinon.mock(segment);
|
||||
const metaMetricsController = getMetaMetricsController({
|
||||
participateInMetaMetrics: false,
|
||||
participateInMetaMetrics: true,
|
||||
});
|
||||
mock
|
||||
.expects('track')
|
||||
@ -388,6 +407,8 @@ describe('MetaMetricsController', function () {
|
||||
test: 1,
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
@ -417,6 +438,8 @@ describe('MetaMetricsController', function () {
|
||||
legacy_event: true,
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
@ -439,12 +462,14 @@ describe('MetaMetricsController', function () {
|
||||
.once()
|
||||
.withArgs({
|
||||
event: 'Fake Event',
|
||||
userId: TEST_META_METRICS_ID,
|
||||
context: DEFAULT_TEST_CONTEXT,
|
||||
properties: {
|
||||
test: 1,
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
context: DEFAULT_TEST_CONTEXT,
|
||||
userId: TEST_META_METRICS_ID,
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.submitEvent({
|
||||
event: 'Fake Event',
|
||||
@ -519,6 +544,8 @@ describe('MetaMetricsController', function () {
|
||||
foo: 'bar',
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
}),
|
||||
);
|
||||
assert.ok(
|
||||
@ -527,6 +554,8 @@ describe('MetaMetricsController', function () {
|
||||
userId: TEST_META_METRICS_ID,
|
||||
context: DEFAULT_TEST_CONTEXT,
|
||||
properties: DEFAULT_EVENT_PROPERTIES,
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -547,6 +576,8 @@ describe('MetaMetricsController', function () {
|
||||
params: null,
|
||||
...DEFAULT_PAGE_PROPERTIES,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.trackPage({
|
||||
name: 'home',
|
||||
@ -590,6 +621,8 @@ describe('MetaMetricsController', function () {
|
||||
params: null,
|
||||
...DEFAULT_PAGE_PROPERTIES,
|
||||
},
|
||||
messageId: Utils.generateRandomId(),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.trackPage(
|
||||
{
|
||||
@ -602,6 +635,50 @@ describe('MetaMetricsController', function () {
|
||||
);
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () {
|
||||
const mock = sinon.mock(segment);
|
||||
const metaMetricsController = getMetaMetricsController({
|
||||
preferencesStore: getMockPreferencesStore({
|
||||
participateInMetaMetrics: null,
|
||||
}),
|
||||
});
|
||||
mock
|
||||
.expects('page')
|
||||
.twice()
|
||||
.withArgs({
|
||||
name: 'home',
|
||||
userId: TEST_META_METRICS_ID,
|
||||
context: DEFAULT_TEST_CONTEXT,
|
||||
properties: {
|
||||
params: null,
|
||||
...DEFAULT_PAGE_PROPERTIES,
|
||||
},
|
||||
messageId: DUMMY_ACTION_ID,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
metaMetricsController.trackPage(
|
||||
{
|
||||
name: 'home',
|
||||
params: null,
|
||||
actionId: DUMMY_ACTION_ID,
|
||||
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
|
||||
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
},
|
||||
{ isOptInPath: true },
|
||||
);
|
||||
metaMetricsController.trackPage(
|
||||
{
|
||||
name: 'home',
|
||||
params: null,
|
||||
actionId: DUMMY_ACTION_ID,
|
||||
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
|
||||
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
},
|
||||
{ isOptInPath: true },
|
||||
);
|
||||
mock.verify();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_buildUserTraitsObject', function () {
|
||||
@ -788,9 +865,35 @@ describe('MetaMetricsController', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitting segmentApiCalls to segment SDK', function () {
|
||||
it('should add event to store when submitting to SDK', function () {
|
||||
const metaMetricsController = getMetaMetricsController({});
|
||||
metaMetricsController.trackPage({}, { isOptIn: true });
|
||||
const { segmentApiCalls } = metaMetricsController.store.getState();
|
||||
assert(Object.keys(segmentApiCalls).length > 0);
|
||||
});
|
||||
|
||||
it('should remove event from store when callback is invoked', function () {
|
||||
const segmentInstance = createSegmentMock(2, 10000);
|
||||
const stubFn = (_, cb) => {
|
||||
cb();
|
||||
};
|
||||
sinon.stub(segmentInstance, 'track').callsFake(stubFn);
|
||||
sinon.stub(segmentInstance, 'page').callsFake(stubFn);
|
||||
|
||||
const metaMetricsController = getMetaMetricsController({
|
||||
segmentInstance,
|
||||
});
|
||||
metaMetricsController.trackPage({}, { isOptIn: true });
|
||||
const { segmentApiCalls } = metaMetricsController.store.getState();
|
||||
assert(Object.keys(segmentApiCalls).length === 0);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// flush the queues manually after each test
|
||||
segment.flush();
|
||||
clock.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ describe('PermissionController specifications', () => {
|
||||
describe('caveat specifications', () => {
|
||||
it('getCaveatSpecifications returns the expected specifications object', () => {
|
||||
const caveatSpecifications = getCaveatSpecifications({});
|
||||
expect(Object.keys(caveatSpecifications)).toHaveLength(4);
|
||||
expect(Object.keys(caveatSpecifications)).toHaveLength(5);
|
||||
expect(
|
||||
caveatSpecifications[CaveatTypes.restrictReturnedAccounts].type,
|
||||
).toStrictEqual(CaveatTypes.restrictReturnedAccounts);
|
||||
@ -30,6 +30,9 @@ describe('PermissionController specifications', () => {
|
||||
expect(caveatSpecifications.snapKeyring.type).toStrictEqual(
|
||||
SnapCaveatType.SnapKeyring,
|
||||
);
|
||||
expect(caveatSpecifications.snapCronjob.type).toStrictEqual(
|
||||
SnapCaveatType.SnapCronjob,
|
||||
);
|
||||
});
|
||||
|
||||
describe('restrictReturnedAccounts', () => {
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
ControllerMessenger,
|
||||
TokenListController,
|
||||
} from '@metamask/controllers';
|
||||
import { ControllerMessenger } from '@metamask/controllers';
|
||||
import { TokenListController } from '@metamask/assets-controllers';
|
||||
import { CHAIN_IDS } from '../../../shared/constants/network';
|
||||
import PreferencesController from './preferences';
|
||||
import NetworkController from './network';
|
||||
|
@ -14,7 +14,10 @@ import log from 'loglevel';
|
||||
import pify from 'pify';
|
||||
import { ethers } from 'ethers';
|
||||
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
|
||||
import { CHAIN_IDS } from '../../../shared/constants/network';
|
||||
import {
|
||||
CHAIN_IDS,
|
||||
LOCALHOST_RPC_URL,
|
||||
} from '../../../shared/constants/network';
|
||||
|
||||
import {
|
||||
SINGLE_CALL_BALANCES_ADDRESS,
|
||||
@ -50,6 +53,7 @@ export default class AccountTracker {
|
||||
* @param {object} opts.provider - An EIP-1193 provider instance that uses the current global network
|
||||
* @param {object} opts.blockTracker - A block tracker, which emits events for each new block
|
||||
* @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
|
||||
* @param {Function} opts.getNetworkIdentifier - A function that returns the current network
|
||||
*/
|
||||
constructor(opts = {}) {
|
||||
const initState = {
|
||||
@ -69,6 +73,7 @@ export default class AccountTracker {
|
||||
// bind function for easier listener syntax
|
||||
this._updateForBlock = this._updateForBlock.bind(this);
|
||||
this.getCurrentChainId = opts.getCurrentChainId;
|
||||
this.getNetworkIdentifier = opts.getNetworkIdentifier;
|
||||
|
||||
this.ethersProvider = new ethers.providers.Web3Provider(this._provider);
|
||||
}
|
||||
@ -199,73 +204,79 @@ export default class AccountTracker {
|
||||
const { accounts } = this.store.getState();
|
||||
const addresses = Object.keys(accounts);
|
||||
const chainId = this.getCurrentChainId();
|
||||
const networkId = this.getNetworkIdentifier();
|
||||
const rpcUrl = 'http://127.0.0.1:8545';
|
||||
|
||||
switch (chainId) {
|
||||
case CHAIN_IDS.MAINNET:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS,
|
||||
);
|
||||
break;
|
||||
if (networkId === LOCALHOST_RPC_URL || networkId === rpcUrl) {
|
||||
await Promise.all(addresses.map(this._updateAccount.bind(this)));
|
||||
} else {
|
||||
switch (chainId) {
|
||||
case CHAIN_IDS.MAINNET:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.GOERLI:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_GOERLI,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.GOERLI:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_GOERLI,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.SEPOLIA:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_SEPOLIA,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.SEPOLIA:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_SEPOLIA,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.BSC:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_BSC,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.BSC:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_BSC,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.OPTIMISM:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_OPTIMISM,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.OPTIMISM:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_OPTIMISM,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.POLYGON:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_POLYGON,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.POLYGON:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_POLYGON,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.AVALANCHE:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_AVALANCHE,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.AVALANCHE:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_AVALANCHE,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.FANTOM:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_FANTOM,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.FANTOM:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_FANTOM,
|
||||
);
|
||||
break;
|
||||
|
||||
case CHAIN_IDS.ARBITRUM:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_ARBITRUM,
|
||||
);
|
||||
break;
|
||||
case CHAIN_IDS.ARBITRUM:
|
||||
await this._updateAccountsViaBalanceChecker(
|
||||
addresses,
|
||||
SINGLE_CALL_BALANCES_ADDRESS_ARBITRUM,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
await Promise.all(addresses.map(this._updateAccount.bind(this)));
|
||||
default:
|
||||
await Promise.all(addresses.map(this._updateAccount.bind(this)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,21 +2,10 @@ import removeSlash from 'remove-trailing-slash';
|
||||
import looselyValidate from '@segment/loosely-validate-event';
|
||||
import { isString } from 'lodash';
|
||||
import isRetryAllowed from 'is-retry-allowed';
|
||||
import { generateRandomId } from '../util';
|
||||
|
||||
const noop = () => ({});
|
||||
|
||||
// Taken from https://stackoverflow.com/a/1349426/3696652
|
||||
const characters =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const generateRandomId = () => {
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Method below is inspired from axios-retry https://github.com/softonic/axios-retry
|
||||
function isNetworkError(error) {
|
||||
return (
|
||||
|
@ -9,6 +9,14 @@ import pump from 'pump';
|
||||
*/
|
||||
export function setupMultiplex(connectionStream) {
|
||||
const mux = new ObjectMultiplex();
|
||||
/**
|
||||
* We are using this streams to send keep alive message between backend/ui without setting up a multiplexer
|
||||
* We need to tell the multiplexer to ignore them, else we get the " orphaned data for stream " warnings
|
||||
* https://github.com/MetaMask/object-multiplex/blob/280385401de84f57ef57054d92cfeb8361ef2680/src/ObjectMultiplex.ts#L63
|
||||
*/
|
||||
mux.ignoreStream('CONNECTION_READY');
|
||||
mux.ignoreStream('ACK_KEEP_ALIVE_MESSAGE');
|
||||
mux.ignoreStream('WORKER_KEEP_ALIVE_MESSAGE');
|
||||
pump(connectionStream, mux, connectionStream, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
|
@ -174,3 +174,19 @@ export {
|
||||
getChainType,
|
||||
checkAlarmExists,
|
||||
};
|
||||
|
||||
// Taken from https://stackoverflow.com/a/1349426/3696652
|
||||
const characters =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
export const generateRandomId = () => {
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const isValidDate = (d) => {
|
||||
return d instanceof Date && !isNaN(d);
|
||||
};
|
||||
|
@ -27,15 +27,10 @@ import {
|
||||
AddressBookController,
|
||||
ApprovalController,
|
||||
ControllerMessenger,
|
||||
CurrencyRateController,
|
||||
PhishingController,
|
||||
AnnouncementController,
|
||||
GasFeeController,
|
||||
TokenListController,
|
||||
TokensController,
|
||||
TokenRatesController,
|
||||
CollectiblesController,
|
||||
AssetsContractController,
|
||||
CollectibleDetectionController,
|
||||
PermissionController,
|
||||
SubjectMetadataController,
|
||||
@ -46,9 +41,17 @@ import {
|
||||
NotificationController,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
} from '@metamask/controllers';
|
||||
import {
|
||||
CurrencyRateController,
|
||||
TokenListController,
|
||||
TokensController,
|
||||
TokenRatesController,
|
||||
AssetsContractController,
|
||||
} from '@metamask/assets-controllers';
|
||||
import SmartTransactionsController from '@metamask/smart-transactions-controller';
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
import {
|
||||
CronjobController,
|
||||
SnapController,
|
||||
IframeExecutionService,
|
||||
} from '@metamask/snap-controllers';
|
||||
@ -296,7 +299,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
onPreferencesStateChange: (listener) =>
|
||||
this.preferencesController.store.subscribe(listener),
|
||||
onNetworkStateChange: (cb) =>
|
||||
this.networkController.store.subscribe((networkState) => {
|
||||
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
||||
const networkState = this.networkController.store.getState();
|
||||
const modifiedNetworkState = {
|
||||
...networkState,
|
||||
provider: {
|
||||
@ -539,6 +543,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||
this.networkController,
|
||||
),
|
||||
getNetworkIdentifier: this.networkController.getNetworkIdentifier.bind(
|
||||
this.networkController,
|
||||
),
|
||||
});
|
||||
|
||||
// start and stop polling for balances based on activeControllerConnections
|
||||
@ -664,7 +671,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
this.snapExecutionService = new IframeExecutionService({
|
||||
iframeUrl: new URL(
|
||||
'https://metamask.github.io/iframe-execution-environment/0.9.1',
|
||||
'https://metamask.github.io/iframe-execution-environment/0.10.0',
|
||||
),
|
||||
messenger: this.controllerMessenger.getRestricted({
|
||||
name: 'ExecutionService',
|
||||
@ -750,6 +757,24 @@ export default class MetamaskController extends EventEmitter {
|
||||
},
|
||||
},
|
||||
});
|
||||
// --- Snaps Cronjob Controller configuration
|
||||
const cronjobControllerMessenger = this.controllerMessenger.getRestricted({
|
||||
name: 'CronjobController',
|
||||
allowedEvents: [
|
||||
'SnapController:snapInstalled',
|
||||
'SnapController:snapUpdated',
|
||||
'SnapController:snapRemoved',
|
||||
],
|
||||
allowedActions: [
|
||||
`${this.permissionController.name}:getPermissions`,
|
||||
'SnapController:handleRequest',
|
||||
'SnapController:getAll',
|
||||
],
|
||||
});
|
||||
this.cronjobController = new CronjobController({
|
||||
state: initState.CronjobController,
|
||||
messenger: cronjobControllerMessenger,
|
||||
});
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
this.detectTokensController = new DetectTokensController({
|
||||
preferences: this.preferencesController,
|
||||
@ -1041,6 +1066,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
CollectiblesController: this.collectiblesController,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
SnapController: this.snapController,
|
||||
CronjobController: this.cronjobController,
|
||||
NotificationController: this.notificationController,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
});
|
||||
@ -1082,6 +1108,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
CollectiblesController: this.collectiblesController,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
SnapController: this.snapController,
|
||||
CronjobController: this.cronjobController,
|
||||
NotificationController: this.notificationController,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
},
|
||||
@ -1130,10 +1157,6 @@ export default class MetamaskController extends EventEmitter {
|
||||
return {
|
||||
...buildSnapEndowmentSpecifications(),
|
||||
...buildSnapRestrictedMethodSpecifications({
|
||||
addSnap: this.controllerMessenger.call.bind(
|
||||
this.controllerMessenger,
|
||||
'SnapController:add',
|
||||
),
|
||||
clearSnapState: this.controllerMessenger.call.bind(
|
||||
this.controllerMessenger,
|
||||
'SnapController:clearSnapState',
|
||||
@ -2042,7 +2065,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async addCustomNetwork(customRpc) {
|
||||
async addCustomNetwork(customRpc, actionId) {
|
||||
const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc;
|
||||
|
||||
await this.preferencesController.addToFrequentRpcList(
|
||||
@ -2078,6 +2101,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
sensitiveProperties: {
|
||||
rpc_url: rpcUrlOrigin,
|
||||
},
|
||||
actionId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import setupSentry from './lib/setupSentry';
|
||||
|
||||
// The root compartment will populate this with hooks
|
||||
global.sentryHooks = {};
|
||||
global.stateHooks = {};
|
||||
|
||||
// setup sentry error reporting
|
||||
global.sentry = setupSentry({
|
||||
release: process.env.METAMASK_VERSION,
|
||||
getState: () => global.sentryHooks?.getSentryState?.() || {},
|
||||
getState: () => global.stateHooks?.getSentryState?.() || {},
|
||||
});
|
||||
|
@ -30,22 +30,65 @@ const container = document.getElementById('app-content');
|
||||
const ONE_SECOND_IN_MILLISECONDS = 1_000;
|
||||
|
||||
const WORKER_KEEP_ALIVE_INTERVAL = ONE_SECOND_IN_MILLISECONDS;
|
||||
// Service Worker Keep Alive Message Constants
|
||||
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
|
||||
const ACK_KEEP_ALIVE_WAIT_TIME = 60_000; // 1 minute
|
||||
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
|
||||
|
||||
// Timeout for initializing phishing warning page.
|
||||
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
|
||||
|
||||
const PHISHING_WARNING_SW_STORAGE_KEY = 'phishing-warning-sw-registered';
|
||||
|
||||
let lastMessageRecievedTimestamp = Date.now();
|
||||
/*
|
||||
* As long as UI is open it will keep sending messages to service worker
|
||||
* In service worker as this message is received
|
||||
* if service worker is inactive it is reactivated and script re-loaded
|
||||
* Time has been kept to 1000ms but can be reduced for even faster re-activation of service worker
|
||||
*/
|
||||
let extensionPort;
|
||||
let timeoutHandle;
|
||||
|
||||
if (isManifestV3) {
|
||||
setInterval(() => {
|
||||
// Checking for SW aliveness (or stuckness) flow
|
||||
// 1. Check if we have an extensionPort, if yes
|
||||
// 2a. Send a keep alive message to the background via extensionPort
|
||||
// 2b. Add a listener to it (if not already added)
|
||||
// 3a. Set a timeout to check if we have received an ACK from background
|
||||
// 3b. If we have not received an ACK within Xs, we know the background is stuck or dead
|
||||
// 4. If we recieve an ACK_KEEP_ALIVE_MESSAGE from the service worker, we know it is alive
|
||||
|
||||
const ackKeepAliveListener = (message) => {
|
||||
if (message.name === ACK_KEEP_ALIVE_MESSAGE) {
|
||||
lastMessageRecievedTimestamp = Date.now();
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
};
|
||||
|
||||
const handle = setInterval(() => {
|
||||
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
|
||||
|
||||
if (extensionPort !== null && extensionPort !== undefined) {
|
||||
extensionPort.postMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
|
||||
|
||||
if (extensionPort.onMessage.hasListener(ackKeepAliveListener) === false) {
|
||||
extensionPort.onMessage.addListener(ackKeepAliveListener);
|
||||
}
|
||||
}
|
||||
|
||||
timeoutHandle = setTimeout(() => {
|
||||
if (
|
||||
Date.now() - lastMessageRecievedTimestamp >
|
||||
ACK_KEEP_ALIVE_WAIT_TIME
|
||||
) {
|
||||
clearInterval(handle);
|
||||
displayCriticalError(
|
||||
'somethingIsWrong',
|
||||
new Error("Something's gone wrong. Try reloading the page."),
|
||||
);
|
||||
}
|
||||
}, ACK_KEEP_ALIVE_WAIT_TIME);
|
||||
}, WORKER_KEEP_ALIVE_INTERVAL);
|
||||
}
|
||||
|
||||
@ -61,7 +104,7 @@ async function start() {
|
||||
let isUIInitialised = false;
|
||||
|
||||
// setup stream to background
|
||||
let extensionPort = browser.runtime.connect({ name: windowType });
|
||||
extensionPort = browser.runtime.connect({ name: windowType });
|
||||
let connectionStream = new PortStream(extensionPort);
|
||||
|
||||
const activeTab = await queryCurrentActiveTab(windowType);
|
||||
@ -208,7 +251,7 @@ async function start() {
|
||||
initializeUi(tab, connectionStream, (err, store) => {
|
||||
if (err) {
|
||||
// if there's an error, store will be = metamaskState
|
||||
displayCriticalError(err, store);
|
||||
displayCriticalError('troubleStarting', err, store);
|
||||
return;
|
||||
}
|
||||
isUIInitialised = true;
|
||||
@ -226,7 +269,7 @@ async function start() {
|
||||
function updateUiStreams() {
|
||||
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
|
||||
if (err) {
|
||||
displayCriticalError(err);
|
||||
displayCriticalError('troubleStarting', err);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -277,8 +320,8 @@ function initializeUi(activeTab, connectionStream, cb) {
|
||||
});
|
||||
}
|
||||
|
||||
async function displayCriticalError(err, metamaskState) {
|
||||
const html = await getErrorHtml(SUPPORT_LINK, metamaskState);
|
||||
async function displayCriticalError(errorKey, err, metamaskState) {
|
||||
const html = await getErrorHtml(errorKey, SUPPORT_LINK, metamaskState);
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
|
@ -71,7 +71,7 @@ async function defineAndRunBuildTasks() {
|
||||
version,
|
||||
} = await parseArgv();
|
||||
|
||||
const browserPlatforms = ['firefox', 'chrome', 'brave', 'opera'];
|
||||
const browserPlatforms = ['firefox', 'chrome'];
|
||||
|
||||
const browserVersionMap = getBrowserVersionMap(browserPlatforms, version);
|
||||
|
||||
|
@ -58,10 +58,8 @@ function createStyleTasks({ livereload }) {
|
||||
};
|
||||
|
||||
async function buildScss() {
|
||||
await Promise.all([
|
||||
buildScssPipeline(src, dest, devMode, false),
|
||||
buildScssPipeline(src, dest, devMode, true),
|
||||
]);
|
||||
await buildScssPipeline(src, dest, devMode, false);
|
||||
await buildScssPipeline(src, dest, devMode, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ async function start() {
|
||||
console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM);
|
||||
const { CIRCLE_WORKFLOW_JOB_ID } = process.env;
|
||||
console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID);
|
||||
const { PARENT_COMMIT } = process.env;
|
||||
console.log('PARENT_COMMIT', PARENT_COMMIT);
|
||||
|
||||
if (!CIRCLE_PULL_REQUEST) {
|
||||
console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`);
|
||||
@ -36,7 +38,7 @@ async function start() {
|
||||
// build the github comment content
|
||||
|
||||
// links to extension builds
|
||||
const platforms = ['chrome', 'firefox', 'opera'];
|
||||
const platforms = ['chrome', 'firefox'];
|
||||
const buildLinks = platforms
|
||||
.map((platform) => {
|
||||
const url = `${BUILD_LINK_BASE}/builds/metamask-${platform}-${VERSION}.zip`;
|
||||
@ -87,6 +89,9 @@ async function start() {
|
||||
.map((key) => `<li>${key}: ${bundles[key].join(', ')}</li>`)
|
||||
.join('')}</ul>`;
|
||||
|
||||
const bundleSizeDataUrl =
|
||||
'https://raw.githubusercontent.com/MetaMask/extension_bundlesize_stats/main/stats/bundle_size_data.json';
|
||||
|
||||
const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`;
|
||||
const coverageLink = `<a href="${coverageUrl}">Report</a>`;
|
||||
|
||||
@ -243,6 +248,67 @@ async function start() {
|
||||
console.log(`No results for ${summaryPlatform} found; skipping benchmark`);
|
||||
}
|
||||
|
||||
try {
|
||||
const prBundleSizeStats = JSON.parse(
|
||||
await fs.readFile(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
path.join('test-artifacts', 'chrome', 'mv3', 'bundle_size.json'),
|
||||
),
|
||||
'utf-8',
|
||||
),
|
||||
);
|
||||
|
||||
const devBundleSizeStats = await (
|
||||
await fetch(bundleSizeDataUrl, {
|
||||
method: 'GET',
|
||||
})
|
||||
).json();
|
||||
|
||||
const prSizes = {
|
||||
background: prBundleSizeStats.background.size,
|
||||
ui: prBundleSizeStats.ui.size,
|
||||
common: prBundleSizeStats.common.size,
|
||||
};
|
||||
|
||||
const devSizes = Object.keys(prSizes).reduce((sizes, part) => {
|
||||
sizes[part] = devBundleSizeStats[PARENT_COMMIT][part] || 0;
|
||||
return sizes;
|
||||
}, {});
|
||||
|
||||
const diffs = Object.keys(prSizes).reduce((output, part) => {
|
||||
output[part] = prSizes[part] - devSizes[part];
|
||||
return output;
|
||||
}, {});
|
||||
|
||||
const sizeDiffRows = Object.keys(diffs).map(
|
||||
(part) => `${part}: ${diffs[part]} bytes`,
|
||||
);
|
||||
|
||||
const sizeDiffHiddenContent = `<ul>${sizeDiffRows
|
||||
.map((row) => `<li>${row}</li>`)
|
||||
.join('\n')}</ul>`;
|
||||
|
||||
const sizeDiff = diffs.background + diffs.common;
|
||||
|
||||
const sizeDiffWarning =
|
||||
sizeDiff > 0
|
||||
? `🚨 Warning! Bundle size has increased!`
|
||||
: `🚀 Bundle size reduced!`;
|
||||
|
||||
const sizeDiffExposedContent =
|
||||
sizeDiff === 0
|
||||
? `Bundle size diffs`
|
||||
: `Bundle size diffs [${sizeDiffWarning}]`;
|
||||
|
||||
const sizeDiffBody = `<details><summary>${sizeDiffExposedContent}</summary>${sizeDiffHiddenContent}</details>\n\n`;
|
||||
|
||||
commentBody += sizeDiffBody;
|
||||
} catch (error) {
|
||||
console.error(`Error constructing bundle size diffs results: '${error}'`);
|
||||
}
|
||||
|
||||
try {
|
||||
const highlights = await getHighlights({ artifactBase: BUILD_LINK_BASE });
|
||||
if (highlights) {
|
||||
|
@ -698,7 +698,7 @@
|
||||
"ui/components/app/signature-request/signature-request-header/signature-request-header.component.js",
|
||||
"ui/components/app/signature-request/signature-request-header/signature-request-header.stories.js",
|
||||
"ui/components/app/signature-request/signature-request-message/index.js",
|
||||
"ui/components/app/signature-request/signature-request-message/signature-request-message.component.js",
|
||||
"ui/components/app/signature-request/signature-request-message/signature-request-message.js",
|
||||
"ui/components/app/signature-request/signature-request.component.js",
|
||||
"ui/components/app/signature-request/signature-request.component.test.js",
|
||||
"ui/components/app/signature-request/signature-request.container.js",
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
Steps to mark a full pass of QA complete.
|
||||
* Browsers: Opera, Chrome, Firefox, Edge.
|
||||
* Use the Chrome build for all Chromium-derived browsers (e.g. Opera and Edge)
|
||||
* OS: Ubuntu, Mac OSX, Windows
|
||||
* Load older version of MetaMask and attempt to simulate updating the extension.
|
||||
* Open Developer Console in background and popup, inspect errors.
|
||||
|
@ -4,12 +4,12 @@ Fixture data can be generated by following these steps:
|
||||
|
||||
1. Load the unpacked extension in development or test mode
|
||||
2. Inspecting the background context of the extension
|
||||
3. Call `metamaskGetState`, then call [`copy`][1] on the results
|
||||
3. Call `stateHooks.metamaskGetState`, then call [`copy`][1] on the results
|
||||
|
||||
You can then paste the contents directly in your fixture file.
|
||||
|
||||
```js
|
||||
copy(await metamaskGetState())
|
||||
copy(await stateHooks.metamaskGetState())
|
||||
```
|
||||
|
||||
|
||||
|
@ -468,6 +468,55 @@
|
||||
"@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.log": true,
|
||||
"setInterval": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/assets-controllers>@metamask/base-controller": true,
|
||||
"@metamask/assets-controllers>@metamask/controller-utils": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/controllers>@ethersproject/abi": true,
|
||||
"@metamask/controllers>@ethersproject/contracts": true,
|
||||
"@metamask/controllers>@ethersproject/providers": true,
|
||||
"@metamask/controllers>abort-controller": true,
|
||||
"@metamask/controllers>async-mutex": true,
|
||||
"@metamask/controllers>multiformats": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"browserify>events": true,
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"ethereumjs-util": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers>@metamask/base-controller": {
|
||||
"packages": {
|
||||
"immer": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers>@metamask/controller-utils": {
|
||||
"globals": {
|
||||
"console.error": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers>isomorphic-fetch": true,
|
||||
"browserify>buffer": true,
|
||||
"eslint>fast-deep-equal": true,
|
||||
"eth-ens-namehash": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethjs>ethjs-unit": true
|
||||
}
|
||||
},
|
||||
"@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
|
@ -613,6 +613,55 @@
|
||||
"@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.log": true,
|
||||
"setInterval": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/assets-controllers>@metamask/base-controller": true,
|
||||
"@metamask/assets-controllers>@metamask/controller-utils": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/controllers>@ethersproject/abi": true,
|
||||
"@metamask/controllers>@ethersproject/contracts": true,
|
||||
"@metamask/controllers>@ethersproject/providers": true,
|
||||
"@metamask/controllers>abort-controller": true,
|
||||
"@metamask/controllers>async-mutex": true,
|
||||
"@metamask/controllers>multiformats": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"browserify>events": true,
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"ethereumjs-util": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers>@metamask/base-controller": {
|
||||
"packages": {
|
||||
"immer": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers>@metamask/controller-utils": {
|
||||
"globals": {
|
||||
"console.error": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers>isomorphic-fetch": true,
|
||||
"browserify>buffer": true,
|
||||
"eslint>fast-deep-equal": true,
|
||||
"eth-ens-namehash": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethjs>ethjs-unit": true
|
||||
}
|
||||
},
|
||||
"@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
@ -1366,6 +1415,50 @@
|
||||
"watchify>xtend": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"WorkerGlobalScope": true,
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/post-message-stream>@metamask/utils": true,
|
||||
"@metamask/post-message-stream>readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream>@metamask/utils": {
|
||||
"packages": {
|
||||
"eslint>fast-deep-equal": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream>readable-stream": {
|
||||
"packages": {
|
||||
"@metamask/post-message-stream>readable-stream>safe-buffer": true,
|
||||
"@metamask/post-message-stream>readable-stream>string_decoder": true,
|
||||
"@storybook/api>util-deprecate": true,
|
||||
"browserify>browser-resolve": true,
|
||||
"browserify>events": true,
|
||||
"browserify>process": true,
|
||||
"browserify>timers-browserify": true,
|
||||
"pumpify>inherits": true,
|
||||
"readable-stream>core-util-is": true,
|
||||
"readable-stream>isarray": true,
|
||||
"vinyl>cloneable-readable>process-nextick-args": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream>readable-stream>safe-buffer": {
|
||||
"packages": {
|
||||
"browserify>buffer": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream>readable-stream>string_decoder": {
|
||||
"packages": {
|
||||
"@metamask/post-message-stream>readable-stream>safe-buffer": true
|
||||
}
|
||||
},
|
||||
"@metamask/providers>@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
@ -1381,7 +1474,7 @@
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers": true,
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/rpc-methods>@metamask/key-tree": true,
|
||||
"@metamask/rpc-methods>nanoid": true,
|
||||
"@metamask/snap-utils": true,
|
||||
@ -1390,137 +1483,6 @@
|
||||
"eth-rpc-errors": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"fetch": true,
|
||||
"setInterval": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@ethereumjs/common": true,
|
||||
"@ethereumjs/tx": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/controllers>@ethersproject/abi": true,
|
||||
"@metamask/controllers>@ethersproject/contracts": true,
|
||||
"@metamask/controllers>@ethersproject/providers": true,
|
||||
"@metamask/controllers>abort-controller": true,
|
||||
"@metamask/controllers>async-mutex": true,
|
||||
"@metamask/controllers>eth-json-rpc-infura": true,
|
||||
"@metamask/controllers>eth-phishing-detect": true,
|
||||
"@metamask/controllers>isomorphic-fetch": true,
|
||||
"@metamask/controllers>multiformats": true,
|
||||
"@metamask/controllers>web3": true,
|
||||
"@metamask/controllers>web3-provider-engine": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry": true,
|
||||
"@metamask/rpc-methods>@metamask/controllers>ethereumjs-wallet": true,
|
||||
"@metamask/rpc-methods>nanoid": true,
|
||||
"browserify>buffer": true,
|
||||
"browserify>events": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eslint>fast-deep-equal": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-keyring-controller": true,
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethjs>ethjs-unit": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"jsonschema": true,
|
||||
"punycode": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry": {
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs": {
|
||||
"globals": {
|
||||
"clearInterval": true,
|
||||
"setInterval": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": true,
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": true,
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": true,
|
||||
"browserify>buffer": true,
|
||||
"ethjs>ethjs-filter": true,
|
||||
"ethjs>ethjs-provider-http": true,
|
||||
"ethjs>ethjs-unit": true,
|
||||
"ethjs>ethjs-util": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"ethjs>number-to-bn": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": {
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
|
||||
"browserify>buffer": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"ethjs>number-to-bn": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": {
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": true,
|
||||
"ethjs-query>babel-runtime": true,
|
||||
"ethjs>ethjs-filter": true,
|
||||
"ethjs>ethjs-util": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"promise-to-callback": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": {
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
|
||||
"browserify>buffer": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"ethjs>number-to-bn": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": {
|
||||
"globals": {
|
||||
"console": true
|
||||
},
|
||||
"packages": {
|
||||
"ethjs-query>babel-runtime": true,
|
||||
"ethjs-query>ethjs-format": true,
|
||||
"ethjs-query>ethjs-rpc": true,
|
||||
"promise-to-callback": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>ethereumjs-wallet": {
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/controllers>ethereumjs-wallet>uuid": true,
|
||||
"@truffle/codec>utf8": true,
|
||||
"browserify>buffer": true,
|
||||
"browserify>crypto-browserify": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-util>ethereum-cryptography": true,
|
||||
"ethereumjs-wallet>aes-js": true,
|
||||
"ethereumjs-wallet>bs58check": true,
|
||||
"ethereumjs-wallet>randombytes": true,
|
||||
"ethers>@ethersproject/json-wallets>scrypt-js": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/controllers>ethereumjs-wallet>uuid": {
|
||||
"globals": {
|
||||
"crypto": true,
|
||||
"msCrypto": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/key-tree": {
|
||||
"packages": {
|
||||
"@metamask/rpc-methods>@metamask/key-tree>@noble/ed25519": true,
|
||||
@ -1528,7 +1490,7 @@
|
||||
"@metamask/rpc-methods>@metamask/key-tree>@scure/bip39": true,
|
||||
"@metamask/snap-utils>@noble/hashes": true,
|
||||
"@metamask/snap-utils>@scure/base": true,
|
||||
"browserify>buffer": true
|
||||
"eth-block-tracker>@metamask/utils": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods>@metamask/key-tree>@noble/ed25519": {
|
||||
@ -1612,11 +1574,11 @@
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/providers>@metamask/object-multiplex": true,
|
||||
"@metamask/rpc-methods": true,
|
||||
"@metamask/snap-controllers>@metamask/browser-passworder": true,
|
||||
"@metamask/snap-controllers>@metamask/controllers": true,
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream": true,
|
||||
"@metamask/snap-controllers>@xstate/fsm": true,
|
||||
"@metamask/snap-controllers>concat-stream": true,
|
||||
"@metamask/snap-controllers>gunzip-maybe": true,
|
||||
@ -1644,176 +1606,6 @@
|
||||
"browserify>buffer": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"fetch": true,
|
||||
"setInterval": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@ethereumjs/common": true,
|
||||
"@ethereumjs/tx": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/controllers>@ethersproject/abi": true,
|
||||
"@metamask/controllers>@ethersproject/contracts": true,
|
||||
"@metamask/controllers>@ethersproject/providers": true,
|
||||
"@metamask/controllers>abort-controller": true,
|
||||
"@metamask/controllers>async-mutex": true,
|
||||
"@metamask/controllers>eth-json-rpc-infura": true,
|
||||
"@metamask/controllers>eth-phishing-detect": true,
|
||||
"@metamask/controllers>isomorphic-fetch": true,
|
||||
"@metamask/controllers>multiformats": true,
|
||||
"@metamask/controllers>web3": true,
|
||||
"@metamask/controllers>web3-provider-engine": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry": true,
|
||||
"@metamask/snap-controllers>@metamask/controllers>ethereumjs-wallet": true,
|
||||
"@metamask/snap-controllers>nanoid": true,
|
||||
"browserify>buffer": true,
|
||||
"browserify>events": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eslint>fast-deep-equal": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-keyring-controller": true,
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethjs>ethjs-unit": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"jsonschema": true,
|
||||
"punycode": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs": {
|
||||
"globals": {
|
||||
"clearInterval": true,
|
||||
"setInterval": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": true,
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": true,
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": true,
|
||||
"browserify>buffer": true,
|
||||
"ethjs>ethjs-filter": true,
|
||||
"ethjs>ethjs-provider-http": true,
|
||||
"ethjs>ethjs-unit": true,
|
||||
"ethjs>ethjs-util": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"ethjs>number-to-bn": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
|
||||
"browserify>buffer": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"ethjs>number-to-bn": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": true,
|
||||
"ethjs-query>babel-runtime": true,
|
||||
"ethjs>ethjs-filter": true,
|
||||
"ethjs>ethjs-util": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"promise-to-callback": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
|
||||
"browserify>buffer": true,
|
||||
"ethjs>js-sha3": true,
|
||||
"ethjs>number-to-bn": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": {
|
||||
"globals": {
|
||||
"console": true
|
||||
},
|
||||
"packages": {
|
||||
"ethjs-query>babel-runtime": true,
|
||||
"ethjs-query>ethjs-format": true,
|
||||
"ethjs-query>ethjs-rpc": true,
|
||||
"promise-to-callback": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>ethereumjs-wallet": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/controllers>ethereumjs-wallet>uuid": true,
|
||||
"@truffle/codec>utf8": true,
|
||||
"browserify>buffer": true,
|
||||
"browserify>crypto-browserify": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-util>ethereum-cryptography": true,
|
||||
"ethereumjs-wallet>aes-js": true,
|
||||
"ethereumjs-wallet>bs58check": true,
|
||||
"ethereumjs-wallet>randombytes": true,
|
||||
"ethers>@ethersproject/json-wallets>scrypt-js": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/controllers>ethereumjs-wallet>uuid": {
|
||||
"globals": {
|
||||
"crypto": true,
|
||||
"msCrypto": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"WorkerGlobalScope": true,
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream>@metamask/utils": true,
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream>@metamask/utils": {
|
||||
"packages": {
|
||||
"eslint>fast-deep-equal": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream>string_decoder": true,
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true,
|
||||
"@storybook/api>util-deprecate": true,
|
||||
"browserify>browser-resolve": true,
|
||||
"browserify>events": true,
|
||||
"browserify>process": true,
|
||||
"browserify>timers-browserify": true,
|
||||
"pumpify>inherits": true,
|
||||
"readable-stream>core-util-is": true,
|
||||
"readable-stream>isarray": true,
|
||||
"vinyl>cloneable-readable>process-nextick-args": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream>string_decoder": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>concat-stream": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>concat-stream>readable-stream": true,
|
||||
@ -1915,38 +1707,8 @@
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream": true,
|
||||
"json-rpc-engine>@metamask/safe-event-emitter": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>process-nextick-args": true,
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true,
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>string_decoder": true,
|
||||
"@storybook/api>util-deprecate": true,
|
||||
"browserify>browser-resolve": true,
|
||||
"browserify>events": true,
|
||||
"browserify>process": true,
|
||||
"browserify>timers-browserify": true,
|
||||
"pumpify>inherits": true,
|
||||
"readable-stream>core-util-is": true,
|
||||
"readable-stream>isarray": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>process-nextick-args": {
|
||||
"packages": {
|
||||
"browserify>process": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": {
|
||||
"packages": {
|
||||
"browserify>buffer": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>string_decoder": {
|
||||
"packages": {
|
||||
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true
|
||||
"json-rpc-engine>@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers>nanoid": {
|
||||
@ -2008,7 +1770,7 @@
|
||||
"@babel/core>@babel/types": true,
|
||||
"@metamask/snap-utils>@noble/hashes": true,
|
||||
"@metamask/snap-utils>@scure/base": true,
|
||||
"@metamask/snap-utils>ajv": true,
|
||||
"@metamask/snap-utils>cron-parser": true,
|
||||
"@metamask/snap-utils>rfdc": true,
|
||||
"@metamask/snap-utils>superstruct": true,
|
||||
"browserify": true,
|
||||
@ -2033,6 +1795,12 @@
|
||||
"TextEncoder": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-utils>cron-parser": {
|
||||
"packages": {
|
||||
"browserify>browser-resolve": true,
|
||||
"luxon": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-utils>rfdc": {
|
||||
"packages": {
|
||||
"browserify>buffer": true
|
||||
|
@ -468,6 +468,55 @@
|
||||
"@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.log": true,
|
||||
"setInterval": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/assets-controllers>@metamask/base-controller": true,
|
||||
"@metamask/assets-controllers>@metamask/controller-utils": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/controllers>@ethersproject/abi": true,
|
||||
"@metamask/controllers>@ethersproject/contracts": true,
|
||||
"@metamask/controllers>@ethersproject/providers": true,
|
||||
"@metamask/controllers>abort-controller": true,
|
||||
"@metamask/controllers>async-mutex": true,
|
||||
"@metamask/controllers>multiformats": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"browserify>events": true,
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"ethereumjs-util": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers>@metamask/base-controller": {
|
||||
"packages": {
|
||||
"immer": true
|
||||
}
|
||||
},
|
||||
"@metamask/assets-controllers>@metamask/controller-utils": {
|
||||
"globals": {
|
||||
"console.error": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers>isomorphic-fetch": true,
|
||||
"browserify>buffer": true,
|
||||
"eslint>fast-deep-equal": true,
|
||||
"eth-ens-namehash": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethjs>ethjs-unit": true
|
||||
}
|
||||
},
|
||||
"@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
|
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "metamask-crx",
|
||||
"version": "10.22.3",
|
||||
"version": "10.23.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -111,6 +111,7 @@
|
||||
"@keystonehq/bc-ur-registry-eth": "^0.12.1",
|
||||
"@keystonehq/metamask-airgapped-keyring": "^0.6.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@metamask/assets-controllers": "^1.0.0",
|
||||
"@metamask/contract-metadata": "^1.31.0",
|
||||
"@metamask/controllers": "^32.0.2",
|
||||
"@metamask/design-tokens": "^1.9.0",
|
||||
@ -122,13 +123,13 @@
|
||||
"@metamask/logo": "^3.1.1",
|
||||
"@metamask/metamask-eth-abis": "^3.0.0",
|
||||
"@metamask/obs-store": "^5.0.0",
|
||||
"@metamask/post-message-stream": "^4.0.0",
|
||||
"@metamask/post-message-stream": "^6.0.0",
|
||||
"@metamask/providers": "^10.0.0",
|
||||
"@metamask/rpc-methods": "^0.22.2",
|
||||
"@metamask/rpc-methods": "^0.23.0",
|
||||
"@metamask/slip44": "^2.1.0",
|
||||
"@metamask/smart-transactions-controller": "^3.0.0",
|
||||
"@metamask/snap-controllers": "^0.22.2",
|
||||
"@metamask/snap-utils": "^0.22.2",
|
||||
"@metamask/snap-controllers": "^0.23.0",
|
||||
"@metamask/snap-utils": "^0.23.0",
|
||||
"@ngraveio/bc-ur": "^1.1.6",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@reduxjs/toolkit": "^1.6.2",
|
||||
@ -187,7 +188,7 @@
|
||||
"localforage": "^1.9.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.4.1",
|
||||
"luxon": "^1.26.0",
|
||||
"luxon": "^3.1.0",
|
||||
"nanoid": "^2.1.6",
|
||||
"nonce-tracker": "^1.0.0",
|
||||
"obj-multiplex": "^1.0.0",
|
||||
@ -325,7 +326,7 @@
|
||||
"fast-glob": "^3.2.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"ganache": "^v7.0.4",
|
||||
"geckodriver": "^1.21.0",
|
||||
"geckodriver": "^3.2.0",
|
||||
"gh-pages": "^3.2.3",
|
||||
"globby": "^11.0.4",
|
||||
"gulp": "^4.0.2",
|
||||
|
@ -1,20 +1,22 @@
|
||||
diff --git a/node_modules/luxon/build/cjs-browser/luxon.js b/node_modules/luxon/build/cjs-browser/luxon.js
|
||||
index 206a47a..5556d1d 100644
|
||||
index 9ab2b9f..14c2891 100644
|
||||
--- a/node_modules/luxon/build/cjs-browser/luxon.js
|
||||
+++ b/node_modules/luxon/build/cjs-browser/luxon.js
|
||||
@@ -7243,13 +7243,13 @@ var DateTime = /*#__PURE__*/function () {
|
||||
@@ -7373,7 +7373,7 @@ var DateTime = /*#__PURE__*/function () {
|
||||
*/
|
||||
;
|
||||
|
||||
- _proto.toLocaleString = function toLocaleString(opts) {
|
||||
+ Reflect.defineProperty(_proto, 'toLocaleString', { value: function toLocaleString(opts) {
|
||||
if (opts === void 0) {
|
||||
opts = DATE_SHORT;
|
||||
- _proto.toLocaleString = function toLocaleString(formatOpts, opts) {
|
||||
+ Reflect.defineProperty(_proto, 'toLocaleString', { value: function toLocaleString(formatOpts, opts) {
|
||||
if (formatOpts === void 0) {
|
||||
formatOpts = DATE_SHORT;
|
||||
}
|
||||
@@ -7383,7 +7383,7 @@ var DateTime = /*#__PURE__*/function () {
|
||||
}
|
||||
|
||||
return this.isValid ? Formatter.create(this.loc.clone(opts), opts).formatDateTime(this) : INVALID$2;
|
||||
return this.isValid ? Formatter.create(this.loc.clone(opts), formatOpts).formatDateTime(this) : INVALID;
|
||||
- }
|
||||
+ }})
|
||||
/**
|
||||
* Returns an array of format "parts", meaning individual tokens along with metadata. This is allows callers to post-process individual sections of the formatted output.
|
||||
* Defaults to the system's locale if no locale has been specified
|
||||
* Defaults to the system's locale if no locale has been specified
|
@ -274,6 +274,7 @@ export const CURRENCY_SYMBOLS = {
|
||||
USDC: 'USDC',
|
||||
USDT: 'USDT',
|
||||
WETH: 'WETH',
|
||||
OPTIMISM: 'OP',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@ -531,6 +532,7 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = {
|
||||
[CURRENCY_SYMBOLS.BNB]: BNB_TOKEN_IMAGE_URL,
|
||||
[CURRENCY_SYMBOLS.MATIC]: MATIC_TOKEN_IMAGE_URL,
|
||||
[CURRENCY_SYMBOLS.AVALANCHE]: AVAX_TOKEN_IMAGE_URL,
|
||||
[CURRENCY_SYMBOLS.OPTIMISM]: OPTIMISM_TOKEN_IMAGE_URL,
|
||||
} as const;
|
||||
|
||||
export const INFURA_BLOCKED_KEY = 'countryBlocked';
|
||||
|
@ -24,6 +24,7 @@ export const EndowmentPermissions = Object.freeze({
|
||||
'endowment:network-access': 'endowment:network-access',
|
||||
'endowment:long-running': 'endowment:long-running',
|
||||
'endowment:transaction-insight': 'endowment:transaction-insight',
|
||||
'endowment:cronjob': 'endowment:cronjob',
|
||||
} as const);
|
||||
|
||||
// Methods / permissions in external packages that we are temporarily excluding.
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ETH_TOKEN_IMAGE_URL,
|
||||
TEST_ETH_TOKEN_IMAGE_URL,
|
||||
BNB_TOKEN_IMAGE_URL,
|
||||
MATIC_TOKEN_IMAGE_URL,
|
||||
@ -23,7 +24,7 @@ export const ETH_SWAPS_TOKEN_OBJECT = {
|
||||
name: 'Ether',
|
||||
address: DEFAULT_TOKEN_ADDRESS,
|
||||
decimals: 18,
|
||||
iconUrl: './images/black-eth-logo.svg',
|
||||
iconUrl: ETH_TOKEN_IMAGE_URL,
|
||||
};
|
||||
|
||||
export const BNB_SWAPS_TOKEN_OBJECT = {
|
||||
@ -66,6 +67,10 @@ export const GOERLI_SWAPS_TOKEN_OBJECT = {
|
||||
iconUrl: TEST_ETH_TOKEN_IMAGE_URL,
|
||||
};
|
||||
|
||||
export const ARBITRUM_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT };
|
||||
|
||||
export const OPTIMISM_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT };
|
||||
|
||||
// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations
|
||||
export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0';
|
||||
|
||||
@ -77,8 +82,9 @@ const BSC_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
|
||||
|
||||
// It's the same as we use for BSC.
|
||||
const POLYGON_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
|
||||
|
||||
const AVALANCHE_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
|
||||
const OPTIMISM_CONTRACT_ADDRESS = '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6';
|
||||
const ARBITRUM_CONTRACT_ADDRESS = '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6';
|
||||
|
||||
export const WETH_CONTRACT_ADDRESS =
|
||||
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
|
||||
@ -91,6 +97,11 @@ export const WMATIC_CONTRACT_ADDRESS =
|
||||
export const WAVAX_CONTRACT_ADDRESS =
|
||||
'0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7';
|
||||
|
||||
export const WETH_OPTIMISM_CONTRACT_ADDRESS =
|
||||
'0x4200000000000000000000000000000000000006';
|
||||
export const WETH_ARBITRUM_CONTRACT_ADDRESS =
|
||||
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1';
|
||||
|
||||
const SWAPS_TESTNET_CHAIN_ID = '0x539';
|
||||
|
||||
export const SWAPS_API_V2_BASE_URL = 'https://swap.metaswap.codefi.network';
|
||||
@ -105,6 +116,8 @@ const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/';
|
||||
const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/';
|
||||
const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/';
|
||||
const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/';
|
||||
const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/';
|
||||
const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/';
|
||||
|
||||
export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [
|
||||
CHAIN_IDS.MAINNET,
|
||||
@ -112,6 +125,8 @@ export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [
|
||||
CHAIN_IDS.BSC,
|
||||
CHAIN_IDS.POLYGON,
|
||||
CHAIN_IDS.AVALANCHE,
|
||||
CHAIN_IDS.OPTIMISM,
|
||||
CHAIN_IDS.ARBITRUM,
|
||||
];
|
||||
|
||||
export const ALLOWED_DEV_SWAPS_CHAIN_IDS = [
|
||||
@ -131,6 +146,8 @@ export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = {
|
||||
[CHAIN_IDS.POLYGON]: POLYGON_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.GOERLI]: TESTNET_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.AVALANCHE]: AVALANCHE_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.OPTIMISM]: OPTIMISM_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.ARBITRUM]: ARBITRUM_CONTRACT_ADDRESS,
|
||||
};
|
||||
|
||||
export const SWAPS_WRAPPED_TOKENS_ADDRESSES = {
|
||||
@ -140,6 +157,8 @@ export const SWAPS_WRAPPED_TOKENS_ADDRESSES = {
|
||||
[CHAIN_IDS.POLYGON]: WMATIC_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.GOERLI]: WETH_GOERLI_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.AVALANCHE]: WAVAX_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.OPTIMISM]: WETH_OPTIMISM_CONTRACT_ADDRESS,
|
||||
[CHAIN_IDS.ARBITRUM]: WETH_ARBITRUM_CONTRACT_ADDRESS,
|
||||
};
|
||||
|
||||
export const ALLOWED_CONTRACT_ADDRESSES = {
|
||||
@ -167,6 +186,14 @@ export const ALLOWED_CONTRACT_ADDRESSES = {
|
||||
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[CHAIN_IDS.AVALANCHE],
|
||||
SWAPS_WRAPPED_TOKENS_ADDRESSES[CHAIN_IDS.AVALANCHE],
|
||||
],
|
||||
[CHAIN_IDS.OPTIMISM]: [
|
||||
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[CHAIN_IDS.OPTIMISM],
|
||||
SWAPS_WRAPPED_TOKENS_ADDRESSES[CHAIN_IDS.OPTIMISM],
|
||||
],
|
||||
[CHAIN_IDS.ARBITRUM]: [
|
||||
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[CHAIN_IDS.ARBITRUM],
|
||||
SWAPS_WRAPPED_TOKENS_ADDRESSES[CHAIN_IDS.ARBITRUM],
|
||||
],
|
||||
};
|
||||
|
||||
export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
|
||||
@ -176,6 +203,8 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
|
||||
[CHAIN_IDS.POLYGON]: MATIC_SWAPS_TOKEN_OBJECT,
|
||||
[CHAIN_IDS.GOERLI]: GOERLI_SWAPS_TOKEN_OBJECT,
|
||||
[CHAIN_IDS.AVALANCHE]: AVAX_SWAPS_TOKEN_OBJECT,
|
||||
[CHAIN_IDS.OPTIMISM]: OPTIMISM_SWAPS_TOKEN_OBJECT,
|
||||
[CHAIN_IDS.ARBITRUM]: ARBITRUM_SWAPS_TOKEN_OBJECT,
|
||||
};
|
||||
|
||||
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
|
||||
@ -184,6 +213,8 @@ export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
|
||||
[CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL,
|
||||
[CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL,
|
||||
[CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL,
|
||||
[CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL,
|
||||
[CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL,
|
||||
};
|
||||
|
||||
export const ETHEREUM = 'ethereum';
|
||||
@ -191,6 +222,8 @@ export const POLYGON = 'polygon';
|
||||
export const BSC = 'bsc';
|
||||
export const GOERLI = 'goerli';
|
||||
export const AVALANCHE = 'avalanche';
|
||||
export const OPTIMISM = 'optimism';
|
||||
export const ARBITRUM = 'arbitrum';
|
||||
|
||||
export const SWAPS_CLIENT_ID = 'extension';
|
||||
|
||||
|
@ -32,7 +32,7 @@ const getLocaleContext = (currentLocaleMessages, enLocaleMessages) => {
|
||||
};
|
||||
};
|
||||
|
||||
export async function getErrorHtml(supportLink, metamaskState) {
|
||||
export async function getErrorHtml(errorKey, supportLink, metamaskState) {
|
||||
let response, preferredLocale;
|
||||
if (metamaskState?.currentLocale) {
|
||||
preferredLocale = metamaskState.currentLocale;
|
||||
@ -50,26 +50,40 @@ export async function getErrorHtml(supportLink, metamaskState) {
|
||||
const { currentLocaleMessages, enLocaleMessages } = response;
|
||||
const t = getLocaleContext(currentLocaleMessages, enLocaleMessages);
|
||||
|
||||
/**
|
||||
* The pattern ${errorKey === 'troubleStarting' ? t('troubleStarting') : ''}
|
||||
* is neccessary because we we need linter to see the string
|
||||
* of the locale keys. If we use the variable directly, the linter will not
|
||||
* see the string and will not be able to check if the locale key exists.
|
||||
*/
|
||||
return `
|
||||
<div class="critical-error">
|
||||
<div class="critical-error__alert">
|
||||
<p class="critical-error__alert__message">
|
||||
${t('troubleStarting')}
|
||||
</p>
|
||||
<button id='critical-error-button' class="critical-error__alert__button">
|
||||
${t('restartMetamask')}
|
||||
</button>
|
||||
<div class="critical-error__icon">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.2325 9.78823L9.14559 1.96347C8.59641 0.910661 7.83651 0.333313 6.99998 0.333313C6.16345 0.333313 5.40354 0.910661 4.85437 1.96347L0.767492 9.78823C0.250247 10.7867 0.192775 11.7444 0.607848 12.4984C1.02292 13.2523 1.8403 13.6666 2.9131 13.6666H11.0869C12.1597 13.6666 12.977 13.2523 13.3921 12.4984C13.8072 11.7444 13.7497 10.7799 13.2325 9.78823ZM6.52105 5.08794C6.52105 4.80945 6.73816 4.57852 6.99998 4.57852C7.26179 4.57852 7.47891 4.80945 7.47891 5.08794V8.4841C7.47891 8.76259 7.26179 8.99353 6.99998 8.99353C6.73816 8.99353 6.52105 8.76259 6.52105 8.4841V5.08794ZM7.45337 11.0041C7.42144 11.0312 7.38951 11.0584 7.35758 11.0856C7.31927 11.1127 7.28095 11.1331 7.24264 11.1467C7.20432 11.1671 7.16601 11.1807 7.12131 11.1874C7.08299 11.1942 7.03829 11.201 6.99998 11.201C6.96166 11.201 6.91696 11.1942 6.87226 11.1874C6.83395 11.1807 6.79563 11.1671 6.75732 11.1467C6.71901 11.1331 6.68069 11.1127 6.64238 11.0856C6.61045 11.0584 6.57852 11.0312 6.54659 11.0041C6.43165 10.875 6.3614 10.6984 6.3614 10.5218C6.3614 10.3452 6.43165 10.1686 6.54659 10.0395C6.57852 10.0124 6.61045 9.98521 6.64238 9.95804C6.68069 9.93087 6.71901 9.91049 6.75732 9.8969C6.79563 9.87653 6.83395 9.86294 6.87226 9.85615C6.95528 9.83577 7.04468 9.83577 7.12131 9.85615C7.16601 9.86294 7.20432 9.87653 7.24264 9.8969C7.28095 9.91049 7.31927 9.93087 7.35758 9.95804C7.38951 9.98521 7.42144 10.0124 7.45337 10.0395C7.56831 10.1686 7.63855 10.3452 7.63855 10.5218C7.63855 10.6984 7.56831 10.875 7.45337 11.0041Z" fill="#F66A0A"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="critical-error__dscription">
|
||||
<div class="critical-error__alert">
|
||||
<p class="critical-error__alert__message">
|
||||
${errorKey === 'troubleStarting' ? t('troubleStarting') : ''}
|
||||
${errorKey === 'somethingIsWrong' ? t('somethingIsWrong') : ''}
|
||||
</p>
|
||||
<span id='critical-error-button' class="critical-error__alert__action-link">
|
||||
${t('restartMetamask')}
|
||||
</span>
|
||||
</div>
|
||||
<p class="critical-error__paragraph">
|
||||
${t('stillGettingMessage')}
|
||||
<a
|
||||
href=${supportLink}
|
||||
class="critical-error__paragraph__link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
${t('sendBugReport')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="critical-error__paragraph">
|
||||
${t('stillGettingMessage')}
|
||||
<a
|
||||
href=${supportLink}
|
||||
class="critical-error__paragraph__link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
${t('sendBugReport')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -33,7 +33,11 @@ describe('Error utils Tests', function () {
|
||||
};
|
||||
|
||||
fetchLocale.mockReturnValue(mockStore.localeMessages.current);
|
||||
const errorHtml = await getErrorHtml(SUPPORT_LINK, mockStore.metamask);
|
||||
const errorHtml = await getErrorHtml(
|
||||
'troubleStarting',
|
||||
SUPPORT_LINK,
|
||||
mockStore.metamask,
|
||||
);
|
||||
const currentLocale = mockStore.localeMessages.current;
|
||||
const troubleStartingMessage = currentLocale.troubleStarting.message;
|
||||
const restartMetamaskMessage = currentLocale.restartMetamask.message;
|
||||
|
@ -146,7 +146,6 @@ export const getBaseApi = function (type, chainId = CHAIN_IDS.MAINNET) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
chainId = TEST_CHAIN_IDS.includes(chainId) ? CHAIN_IDS.MAINNET : chainId;
|
||||
const baseUrl = getBaseUrlForNewSwapsApi(type, chainId);
|
||||
const chainIdDecimal = chainId && parseInt(chainId, 16);
|
||||
if (!baseUrl) {
|
||||
throw new Error(`Swaps API calls are disabled for chainId: ${chainId}`);
|
||||
}
|
||||
@ -164,8 +163,7 @@ export const getBaseApi = function (type, chainId = CHAIN_IDS.MAINNET) {
|
||||
case 'gasPrices':
|
||||
return `${baseUrl}/gasPrices`;
|
||||
case 'network':
|
||||
// Only use v2 for this endpoint.
|
||||
return `${SWAPS_API_V2_BASE_URL}/networks/${chainIdDecimal}`;
|
||||
return baseUrl;
|
||||
default:
|
||||
throw new Error('getBaseApi requires an api call type');
|
||||
}
|
||||
|
@ -12,12 +12,15 @@
|
||||
"previousModalState": {
|
||||
"name": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": null
|
||||
},
|
||||
"history": {
|
||||
"mostRecentOverviewPage": "/"
|
||||
"mostRecentOverviewPage": "/mostRecentOverviewPage"
|
||||
},
|
||||
"metamask": {
|
||||
"usePhishDetect": true,
|
||||
"participateInMetaMetrics": false,
|
||||
"gasEstimateType": "fee-market",
|
||||
"showBetaHeader": false,
|
||||
"gasFeeEstimates": {
|
||||
@ -47,6 +50,7 @@
|
||||
"priorityFeeTrend": "down",
|
||||
"networkCongestion": 0.90625
|
||||
},
|
||||
"snaps": [{}],
|
||||
"preferences": {
|
||||
"hideZeroBalanceTokens": false,
|
||||
"showFiatInTestnets": false,
|
||||
@ -242,20 +246,6 @@
|
||||
"unapprovedEncryptionPublicKeyMsgCount": 0,
|
||||
"unapprovedTypedMessages": {},
|
||||
"unapprovedTypedMessagesCount": 0,
|
||||
"send": {
|
||||
"gasLimit": "0x5208",
|
||||
"gasPrice": "0xee6b2800",
|
||||
"gasTotal": "0x4c65c6294000",
|
||||
"tokenBalance": null,
|
||||
"from": "0xc42edfcc21ed14dda456aa0756c153f7985d8813",
|
||||
"to": "",
|
||||
"amount": "1bc16d674ec80000",
|
||||
"memo": "",
|
||||
"errors": {},
|
||||
"maxModeOn": false,
|
||||
"editingTransactionId": null,
|
||||
"toNickname": ""
|
||||
},
|
||||
"useTokenDetection": true,
|
||||
"advancedGasFee": {
|
||||
"maxBaseFee": "75",
|
||||
@ -1285,5 +1275,24 @@
|
||||
"origin": "tmashuang.github.io"
|
||||
}
|
||||
]
|
||||
},
|
||||
"send": {
|
||||
"amountMode": "INPUT",
|
||||
"currentTransactionUUID": null,
|
||||
"draftTransactions": {},
|
||||
"eip1559support": false,
|
||||
"gasEstimateIsLoading": true,
|
||||
"gasEstimatePollToken": null,
|
||||
"gasIsSetInModal": false,
|
||||
"gasPriceEstimate": "0x0",
|
||||
"gasLimitMinimum": "0x5208",
|
||||
"gasTotalForLayer1": "0x0",
|
||||
"recipientMode": "CONTACT_LIST",
|
||||
"recipientInput": "",
|
||||
"selectedAccount": {
|
||||
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
|
||||
"balance": "0x0"
|
||||
},
|
||||
"stage": "INACTIVE"
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,8 @@ async function withFixtures(options, testSuite) {
|
||||
const phishingPageServer = new PhishingWarningPageServer();
|
||||
|
||||
let webDriver;
|
||||
let driver;
|
||||
const errors = [];
|
||||
let failed = false;
|
||||
try {
|
||||
await ganacheServer.start(ganacheOptions);
|
||||
@ -110,8 +112,12 @@ async function withFixtures(options, testSuite) {
|
||||
) {
|
||||
await ensureXServerIsRunning();
|
||||
}
|
||||
const { driver } = await buildWebDriver(driverOptions);
|
||||
webDriver = driver;
|
||||
driver = (await buildWebDriver(driverOptions)).driver;
|
||||
webDriver = driver.driver;
|
||||
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
await driver.checkBrowserForExceptions();
|
||||
}
|
||||
|
||||
await testSuite({
|
||||
driver,
|
||||
@ -120,7 +126,7 @@ async function withFixtures(options, testSuite) {
|
||||
});
|
||||
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
const errors = await driver.checkBrowserForConsoleErrors(driver);
|
||||
errors.concat(await driver.checkBrowserForConsoleErrors(driver));
|
||||
if (errors.length) {
|
||||
const errorReports = errors.map((err) => err.message);
|
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join(
|
||||
@ -137,10 +143,20 @@ async function withFixtures(options, testSuite) {
|
||||
failed = true;
|
||||
if (webDriver) {
|
||||
try {
|
||||
await webDriver.verboseReportOnFailure(title);
|
||||
await driver.verboseReportOnFailure(title);
|
||||
} catch (verboseReportError) {
|
||||
console.error(verboseReportError);
|
||||
}
|
||||
if (
|
||||
errors.length === 0 &&
|
||||
driver.exceptions.length > 0 &&
|
||||
failOnConsoleError
|
||||
) {
|
||||
const errorMessage = `Errors found in browser console:\n${driver.exceptions.join(
|
||||
'\n',
|
||||
)}`;
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@ -151,7 +167,7 @@ async function withFixtures(options, testSuite) {
|
||||
await secondaryGanacheServer.quit();
|
||||
}
|
||||
if (webDriver) {
|
||||
await webDriver.quit();
|
||||
await driver.quit();
|
||||
}
|
||||
if (dapp) {
|
||||
for (let i = 0; i < numberOfDapps; i++) {
|
||||
|
@ -458,10 +458,10 @@ describe('MetaMask', function () {
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement({ text: 'Edit', tag: 'button' });
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement(
|
||||
{ text: 'Edit suggested gas fee', tag: 'button' },
|
||||
10000,
|
||||
);
|
||||
await driver.clickElement({
|
||||
text: 'Edit suggested gas fee',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
const inputs = await driver.findElements('input[type="number"]');
|
||||
const gasLimitInput = inputs[0];
|
||||
@ -576,10 +576,10 @@ describe('MetaMask', function () {
|
||||
it('customizes gas', async function () {
|
||||
await driver.clickElement('.confirm-approve-content__small-blue-text');
|
||||
await driver.delay(regularDelayMs);
|
||||
await driver.clickElement(
|
||||
{ text: 'Edit suggested gas fee', tag: 'button' },
|
||||
10000,
|
||||
);
|
||||
await driver.clickElement({
|
||||
text: 'Edit suggested gas fee',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.delay(regularDelayMs);
|
||||
|
||||
const [gasLimitInput, gasPriceInput] = await driver.findElements(
|
||||
|
@ -13,6 +13,14 @@ const getTestPathsForTestDir = async (testDir) => {
|
||||
return testPaths;
|
||||
};
|
||||
|
||||
function chunk(array, chunkSize) {
|
||||
const result = [];
|
||||
for (let i = 0; i < array.length; i += chunkSize) {
|
||||
result.push(array.slice(i, i + chunkSize));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { argv } = yargs(hideBin(process.argv))
|
||||
.usage(
|
||||
@ -66,7 +74,14 @@ async function main() {
|
||||
args.push('--retries', retries);
|
||||
}
|
||||
|
||||
for (const testPath of testPaths) {
|
||||
// For running E2Es in parallel in CI
|
||||
const currentChunkIndex = process.env.CIRCLE_NODE_INDEX ?? 0;
|
||||
const totalChunks = process.env.CIRCLE_NODE_TOTAL ?? 1;
|
||||
const chunkSize = Math.ceil(testPaths.length / totalChunks);
|
||||
const chunks = chunk(testPaths, chunkSize);
|
||||
const currentChunk = chunks[currentChunkIndex];
|
||||
|
||||
for (const testPath of currentChunk) {
|
||||
await runInShell('node', [...args, testPath]);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
module.exports = {
|
||||
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/3.1.0',
|
||||
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/3.2.0',
|
||||
};
|
||||
|
@ -136,7 +136,7 @@ describe('Test Snap bip-32', function () {
|
||||
);
|
||||
assert.equal(
|
||||
await publicKeyResult.getText(),
|
||||
'Public key: "043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"',
|
||||
'Public key: "0x043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"',
|
||||
);
|
||||
|
||||
// wait then run compressed public key test
|
||||
@ -149,7 +149,7 @@ describe('Test Snap bip-32', function () {
|
||||
);
|
||||
assert.equal(
|
||||
await compressedPublicKeyResult.getText(),
|
||||
'Public key: "033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"',
|
||||
'Public key: "0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -221,10 +221,10 @@ describe.skip('Create token, approve token and approve token without gas', funct
|
||||
await driver.clickElement(
|
||||
'.confirm-approve-content__small-blue-text',
|
||||
);
|
||||
await driver.clickElement(
|
||||
{ text: 'Edit suggested gas fee', tag: 'button' },
|
||||
10000,
|
||||
);
|
||||
await driver.clickElement({
|
||||
text: 'Edit suggested gas fee',
|
||||
tag: 'button',
|
||||
});
|
||||
const [gasLimitInput, gasPriceInput] = await driver.findElements(
|
||||
'input[type="number"]',
|
||||
);
|
||||
|
@ -135,7 +135,7 @@ describe('Send ETH non-contract address with data that matches ERC20 transfer da
|
||||
|
||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||
|
||||
await driver.clickElement({ text: '0xc42...cd28' });
|
||||
await driver.clickElement({ text: 'New contract' });
|
||||
|
||||
const recipientAddress = await driver.findElements({
|
||||
text: '0xc427D562164062a23a5cFf596A4a3208e72Acd28',
|
||||
@ -239,23 +239,6 @@ describe('Send ETH from dapp using advanced gas controls', function () {
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
// goes to the settings screen
|
||||
await driver.clickElement('.account-menu__icon');
|
||||
await driver.clickElement({ text: 'Settings', tag: 'div' });
|
||||
await driver.clickElement({ text: 'Advanced', tag: 'div' });
|
||||
await driver.clickElement(
|
||||
'[data-testid="advanced-setting-show-testnet-conversion"] .settings-page__content-item-col > label > div',
|
||||
);
|
||||
const advancedGasTitle = await driver.findElement({
|
||||
text: 'Advanced gas controls',
|
||||
tag: 'span',
|
||||
});
|
||||
await driver.scrollToElement(advancedGasTitle);
|
||||
await driver.clickElement(
|
||||
'[data-testid="advanced-setting-advanced-gas-inline"] .settings-page__content-item-col > label > div',
|
||||
);
|
||||
await driver.clickElement('.app-header__logo-container');
|
||||
|
||||
// initiates a send from the dapp
|
||||
await driver.openNewPage('http://127.0.0.1:8080/');
|
||||
await driver.clickElement({ text: 'Send', tag: 'button' });
|
||||
@ -272,10 +255,10 @@ describe('Send ETH from dapp using advanced gas controls', function () {
|
||||
css: '.transaction-total-banner',
|
||||
text: '0.00021 ETH',
|
||||
});
|
||||
await driver.clickElement(
|
||||
{ text: 'Edit suggested gas fee', tag: 'button' },
|
||||
10000,
|
||||
);
|
||||
await driver.clickElement({
|
||||
text: 'Edit suggested gas fee',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.waitForSelector({
|
||||
css: '.transaction-total-banner',
|
||||
text: '0.00021 ETH',
|
||||
|
@ -57,7 +57,7 @@ describe('Send ETH to a 40 character hexadecimal address', function () {
|
||||
);
|
||||
await sendTransactionListItem.click();
|
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
|
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
|
||||
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
|
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement(
|
||||
@ -108,7 +108,7 @@ describe('Send ETH to a 40 character hexadecimal address', function () {
|
||||
);
|
||||
await sendTransactionListItem.click();
|
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
|
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
|
||||
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
|
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement(
|
||||
@ -212,7 +212,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
|
||||
);
|
||||
await sendTransactionListItem.click();
|
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
|
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
|
||||
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
|
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement(
|
||||
@ -302,7 +302,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
|
||||
);
|
||||
await sendTransactionListItem.click();
|
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
|
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
|
||||
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
|
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement(
|
||||
|
@ -56,7 +56,7 @@ describe('Sign Typed Data V4 Signature Request', function () {
|
||||
const origin = content[0];
|
||||
const address = content[1];
|
||||
const message = await driver.findElement(
|
||||
'.signature-request-message--node-value',
|
||||
'.signature-request-data__node__value',
|
||||
);
|
||||
assert.equal(await title.getText(), 'Signature request');
|
||||
assert.equal(await name.getText(), 'Ether Mail');
|
||||
@ -140,7 +140,7 @@ describe('Sign Typed Data V3 Signature Request', function () {
|
||||
const origin = content[0];
|
||||
const address = content[1];
|
||||
const messages = await driver.findElements(
|
||||
'.signature-request-message--node-value',
|
||||
'.signature-request-data__node__value',
|
||||
);
|
||||
assert.equal(await title.getText(), 'Signature request');
|
||||
assert.equal(await name.getText(), 'Ether Mail');
|
||||
@ -154,6 +154,10 @@ describe('Sign Typed Data V3 Signature Request', function () {
|
||||
assert.equal(await messages[4].getText(), 'Hello, Bob!');
|
||||
|
||||
// Approve signing typed data
|
||||
await driver.clickElement(
|
||||
'[data-testid="signature-request-scroll-button"]',
|
||||
);
|
||||
await driver.delay(regularDelayMs);
|
||||
await driver.clickElement({ text: 'Sign', tag: 'button' });
|
||||
await driver.waitUntilXWindowHandles(2);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
|
@ -49,6 +49,7 @@ class Driver {
|
||||
this.browser = browser;
|
||||
this.extensionUrl = extensionUrl;
|
||||
this.timeout = timeout;
|
||||
this.exceptions = [];
|
||||
// The following values are found in
|
||||
// https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/lib/input.js#L50-L110
|
||||
// These should be replaced with string constants 'Enter' etc for playwright.
|
||||
@ -414,7 +415,9 @@ class Driver {
|
||||
const htmlSource = await this.driver.getPageSource();
|
||||
await fs.writeFile(`${filepathBase}-dom.html`, htmlSource);
|
||||
const uiState = await this.driver.executeScript(
|
||||
() => window.getCleanAppState && window.getCleanAppState(),
|
||||
() =>
|
||||
window.stateHooks.getCleanAppState &&
|
||||
window.stateHooks.getCleanAppState(),
|
||||
);
|
||||
await fs.writeFile(
|
||||
`${filepathBase}-state.json`,
|
||||
@ -436,6 +439,15 @@ class Driver {
|
||||
return browserLogs;
|
||||
}
|
||||
|
||||
async checkBrowserForExceptions() {
|
||||
const { exceptions } = this;
|
||||
const cdpConnection = await this.driver.createCDPConnection('page');
|
||||
await this.driver.onLogException(cdpConnection, function (exception) {
|
||||
const { description } = exception.exceptionDetails.exception;
|
||||
exceptions.push(description);
|
||||
});
|
||||
}
|
||||
|
||||
async checkBrowserForConsoleErrors() {
|
||||
const ignoredLogTypes = ['WARNING'];
|
||||
const ignoredErrorMessages = [
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* eslint-disable-next-line */
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
import nock from 'nock';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
@ -102,6 +104,10 @@ if (!window.crypto.getRandomValues) {
|
||||
window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues');
|
||||
}
|
||||
|
||||
// TextEncoder/TextDecoder
|
||||
window.TextEncoder = TextEncoder;
|
||||
window.TextDecoder = TextDecoder;
|
||||
|
||||
// Used to test `clearClipboard` function
|
||||
if (!window.navigator.clipboard) {
|
||||
window.navigator.clipboard = {};
|
||||
|
@ -103,6 +103,9 @@ const createGetSmartTransactionFeesApiResponse = () => {
|
||||
|
||||
export const createSwapsMockStore = () => {
|
||||
return {
|
||||
confirmTransaction: {
|
||||
txData: {},
|
||||
},
|
||||
swaps: {
|
||||
customGas: {
|
||||
limit: '0x0',
|
||||
@ -144,6 +147,76 @@ export const createSwapsMockStore = () => {
|
||||
showFiatInTestnets: true,
|
||||
},
|
||||
currentCurrency: 'ETH',
|
||||
currentNetworkTxList: [
|
||||
{
|
||||
id: 6571648590592143,
|
||||
time: 1667403993369,
|
||||
status: 'confirmed',
|
||||
metamaskNetworkId: '5',
|
||||
originalGasEstimate: '0x7548',
|
||||
userEditedGasLimit: false,
|
||||
chainId: '0x5',
|
||||
loadingDefaults: false,
|
||||
dappSuggestedGasFees: null,
|
||||
sendFlowHistory: null,
|
||||
txParams: {
|
||||
from: '0x806627172af48bd5b0765d3449a7def80d6576ff',
|
||||
to: '0x881d40237659c251811cec9c364ef91dc08d300c',
|
||||
nonce: '0x30',
|
||||
value: '0x5af3107a4000',
|
||||
gas: '0x7548',
|
||||
maxFeePerGas: '0x19286f704d',
|
||||
maxPriorityFeePerGas: '0x77359400',
|
||||
},
|
||||
origin: 'metamask',
|
||||
actionId: 1667403993358.877,
|
||||
type: 'swap',
|
||||
userFeeLevel: 'medium',
|
||||
defaultGasEstimates: {
|
||||
estimateType: 'medium',
|
||||
gas: '0x7548',
|
||||
maxFeePerGas: '0x19286f704d',
|
||||
maxPriorityFeePerGas: '0x77359400',
|
||||
},
|
||||
sourceTokenSymbol: 'ETH',
|
||||
destinationTokenSymbol: 'USDC',
|
||||
destinationTokenDecimals: 6,
|
||||
destinationTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
swapMetaData: {
|
||||
token_from: 'ETH',
|
||||
token_from_amount: '0.0001',
|
||||
token_to: 'USDC',
|
||||
token_to_amount: '0.15471500',
|
||||
slippage: 2,
|
||||
custom_slippage: false,
|
||||
best_quote_source: 'pmm',
|
||||
other_quote_selected: false,
|
||||
other_quote_selected_source: '',
|
||||
gas_fees: '3.016697',
|
||||
estimated_gas: '30024',
|
||||
used_gas_price: '0',
|
||||
is_hardware_wallet: false,
|
||||
stx_enabled: false,
|
||||
current_stx_enabled: false,
|
||||
stx_user_opt_in: false,
|
||||
reg_tx_fee_in_usd: 3.02,
|
||||
reg_tx_fee_in_eth: 0.00193,
|
||||
reg_tx_max_fee_in_usd: 5.06,
|
||||
reg_tx_max_fee_in_eth: 0.00324,
|
||||
max_fee_per_gas: '19286f704d',
|
||||
max_priority_fee_per_gas: '77359400',
|
||||
base_and_priority_fee_per_gas: 'efd93d95a',
|
||||
},
|
||||
swapTokenValue: '0.0001',
|
||||
estimatedBaseFee: 'e865e455a',
|
||||
hash: '0x8216e3696e7deb7ca794703015f17d5114a09362ae98f6a1611203e4c9509243',
|
||||
submittedTime: 1667403996143,
|
||||
firstRetryBlockNumber: '0x7838fe',
|
||||
baseFeePerGas: '0xe0ef7d207',
|
||||
blockTimestamp: '636290e8',
|
||||
postTxBalance: '19a61aaaf06e4bd1',
|
||||
},
|
||||
],
|
||||
conversionRate: 1,
|
||||
contractExchangeRates: {
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2,
|
||||
|
@ -217,25 +217,88 @@ export default function ApproveContentCard({
|
||||
}
|
||||
|
||||
ApproveContentCard.propTypes = {
|
||||
/**
|
||||
* Whether to show header including icon, transaction fee text and edit button
|
||||
*/
|
||||
showHeader: PropTypes.bool,
|
||||
/**
|
||||
* Symbol icon
|
||||
*/
|
||||
symbol: PropTypes.node,
|
||||
/**
|
||||
* Title to be included in the header
|
||||
*/
|
||||
title: PropTypes.string,
|
||||
/**
|
||||
* Whether to show edit button or not
|
||||
*/
|
||||
showEdit: PropTypes.bool,
|
||||
/**
|
||||
* Whether to show advanced gas fee options or not
|
||||
*/
|
||||
showAdvanceGasFeeOptions: PropTypes.bool,
|
||||
/**
|
||||
* Should open customize gas modal when edit button is clicked
|
||||
*/
|
||||
onEditClick: PropTypes.func,
|
||||
/**
|
||||
* Footer to be shown
|
||||
*/
|
||||
footer: PropTypes.node,
|
||||
/**
|
||||
* Whether to include border-bottom or not
|
||||
*/
|
||||
noBorder: PropTypes.bool,
|
||||
/**
|
||||
* Is enhanced gas fee enabled or not
|
||||
*/
|
||||
supportsEIP1559V2: PropTypes.bool,
|
||||
/**
|
||||
* Whether to render transaction details content or not
|
||||
*/
|
||||
renderTransactionDetailsContent: PropTypes.bool,
|
||||
/**
|
||||
* Whether to render data content or not
|
||||
*/
|
||||
renderDataContent: PropTypes.bool,
|
||||
/**
|
||||
* Is multi-layer fee network or not
|
||||
*/
|
||||
isMultiLayerFeeNetwork: PropTypes.bool,
|
||||
/**
|
||||
* Total sum of the transaction in native currency
|
||||
*/
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
/**
|
||||
* Current native currency
|
||||
*/
|
||||
nativeCurrency: PropTypes.string,
|
||||
/**
|
||||
* Current transaction
|
||||
*/
|
||||
fullTxData: PropTypes.object,
|
||||
/**
|
||||
* Total sum of the transaction converted to hex value
|
||||
*/
|
||||
hexTransactionTotal: PropTypes.string,
|
||||
/**
|
||||
* Total sum of the transaction in fiat currency
|
||||
*/
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
/**
|
||||
* Current fiat currency
|
||||
*/
|
||||
currentCurrency: PropTypes.string,
|
||||
/**
|
||||
* Is set approve for all or not
|
||||
*/
|
||||
isSetApproveForAll: PropTypes.bool,
|
||||
/**
|
||||
* Whether a current set approval for all transaction will approve or revoke access
|
||||
*/
|
||||
isApprovalOrRejection: PropTypes.bool,
|
||||
/**
|
||||
* Current transaction data
|
||||
*/
|
||||
data: PropTypes.string,
|
||||
};
|
||||
|
@ -0,0 +1,196 @@
|
||||
import React from 'react';
|
||||
import ApproveContentCard from './approve-content-card';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/ApproveContentCard',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
showHeader: {
|
||||
control: 'boolean',
|
||||
},
|
||||
symbol: {
|
||||
control: 'array',
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
},
|
||||
showEdit: {
|
||||
control: 'boolean',
|
||||
},
|
||||
showAdvanceGasFeeOptions: {
|
||||
control: 'boolean',
|
||||
},
|
||||
footer: {
|
||||
control: 'array',
|
||||
},
|
||||
noBorder: {
|
||||
control: 'boolean',
|
||||
},
|
||||
supportsEIP1559V2: {
|
||||
control: 'boolean',
|
||||
},
|
||||
renderTransactionDetailsContent: {
|
||||
control: 'boolean',
|
||||
},
|
||||
renderDataContent: {
|
||||
control: 'boolean',
|
||||
},
|
||||
isMultiLayerFeeNetwork: {
|
||||
control: 'boolean',
|
||||
},
|
||||
ethTransactionTotal: {
|
||||
control: 'text',
|
||||
},
|
||||
nativeCurrency: {
|
||||
control: 'text',
|
||||
},
|
||||
fullTxData: {
|
||||
control: 'object',
|
||||
},
|
||||
hexTransactionTotal: {
|
||||
control: 'text',
|
||||
},
|
||||
fiatTransactionTotal: {
|
||||
control: 'text',
|
||||
},
|
||||
currentCurrency: {
|
||||
control: 'text',
|
||||
},
|
||||
isSetApproveForAll: {
|
||||
control: 'boolean',
|
||||
},
|
||||
isApprovalOrRejection: {
|
||||
control: 'boolean',
|
||||
},
|
||||
data: {
|
||||
control: 'text',
|
||||
},
|
||||
onEditClick: {
|
||||
control: 'onEditClick',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
showHeader: true,
|
||||
symbol: <i className="fa fa-tag" />,
|
||||
title: 'Transaction fee',
|
||||
showEdit: true,
|
||||
showAdvanceGasFeeOptions: true,
|
||||
noBorder: true,
|
||||
supportsEIP1559V2: false,
|
||||
renderTransactionDetailsContent: true,
|
||||
renderDataContent: false,
|
||||
isMultiLayerFeeNetwork: false,
|
||||
ethTransactionTotal: '0.0012',
|
||||
nativeCurrency: 'GoerliETH',
|
||||
hexTransactionTotal: '0x44364c5bb0000',
|
||||
fiatTransactionTotal: '1.54',
|
||||
currentCurrency: 'usd',
|
||||
isSetApproveForAll: false,
|
||||
isApprovalOrRejection: false,
|
||||
data: '',
|
||||
fullTxData: {
|
||||
id: 3049568294499567,
|
||||
time: 1664449552289,
|
||||
status: 'unapproved',
|
||||
metamaskNetworkId: '3',
|
||||
originalGasEstimate: '0xea60',
|
||||
userEditedGasLimit: false,
|
||||
chainId: '0x3',
|
||||
loadingDefaults: false,
|
||||
dappSuggestedGasFees: {
|
||||
gasPrice: '0x4a817c800',
|
||||
gas: '0xea60',
|
||||
},
|
||||
sendFlowHistory: [],
|
||||
txParams: {
|
||||
from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
|
||||
to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
|
||||
value: '0x0',
|
||||
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
|
||||
gas: '0xea60',
|
||||
maxFeePerGas: '0x4a817c800',
|
||||
maxPriorityFeePerGas: '0x4a817c800',
|
||||
},
|
||||
origin: 'https://metamask.github.io',
|
||||
type: 'approve',
|
||||
history: [
|
||||
{
|
||||
id: 3049568294499567,
|
||||
time: 1664449552289,
|
||||
status: 'unapproved',
|
||||
metamaskNetworkId: '3',
|
||||
originalGasEstimate: '0xea60',
|
||||
userEditedGasLimit: false,
|
||||
chainId: '0x3',
|
||||
loadingDefaults: true,
|
||||
dappSuggestedGasFees: {
|
||||
gasPrice: '0x4a817c800',
|
||||
gas: '0xea60',
|
||||
},
|
||||
sendFlowHistory: [],
|
||||
txParams: {
|
||||
from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
|
||||
to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
|
||||
value: '0x0',
|
||||
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
|
||||
gas: '0xea60',
|
||||
gasPrice: '0x4a817c800',
|
||||
},
|
||||
origin: 'https://metamask.github.io',
|
||||
type: 'approve',
|
||||
},
|
||||
[
|
||||
{
|
||||
op: 'remove',
|
||||
path: '/txParams/gasPrice',
|
||||
note: 'Added new unapproved transaction.',
|
||||
timestamp: 1664449553939,
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/txParams/maxFeePerGas',
|
||||
value: '0x4a817c800',
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/txParams/maxPriorityFeePerGas',
|
||||
value: '0x4a817c800',
|
||||
},
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/loadingDefaults',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/userFeeLevel',
|
||||
value: 'custom',
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/defaultGasEstimates',
|
||||
value: {
|
||||
estimateType: 'custom',
|
||||
gas: '0xea60',
|
||||
maxFeePerGas: '0x4a817c800',
|
||||
maxPriorityFeePerGas: '0x4a817c800',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
userFeeLevel: 'custom',
|
||||
defaultGasEstimates: {
|
||||
estimateType: 'custom',
|
||||
gas: '0xea60',
|
||||
maxFeePerGas: '0x4a817c800',
|
||||
maxPriorityFeePerGas: '0x4a817c800',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => {
|
||||
return <ApproveContentCard {...args} />;
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -92,7 +92,7 @@ describe('Confirm Page Container Container Test', () => {
|
||||
expect(senderRecipient).toBeInTheDocument();
|
||||
});
|
||||
it('should render recipient as address', () => {
|
||||
const recipientName = screen.queryByText(shortenAddress(props.toAddress));
|
||||
const recipientName = screen.queryByText('New contract');
|
||||
expect(recipientName).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -118,7 +118,7 @@ describe('Confirm Page Container Container Test', () => {
|
||||
|
||||
describe('Contact/AddressBook name should appear in recipient header', () => {
|
||||
it('should not show add to address dialog if recipient is in contact list and should display contact name', () => {
|
||||
const addressBookName = 'test save name';
|
||||
const addressBookName = 'New contract';
|
||||
|
||||
const addressBook = {
|
||||
'0x5': {
|
||||
|
@ -65,6 +65,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
fromName: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
toName: PropTypes.string,
|
||||
toMetadataName: PropTypes.string,
|
||||
toEns: PropTypes.string,
|
||||
toNickname: PropTypes.string,
|
||||
// Content
|
||||
@ -119,6 +120,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
fromName,
|
||||
fromAddress,
|
||||
toName,
|
||||
toMetadataName,
|
||||
toEns,
|
||||
toNickname,
|
||||
toAddress,
|
||||
@ -233,6 +235,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
senderName={fromName}
|
||||
senderAddress={fromAddress}
|
||||
recipientName={toName}
|
||||
recipientMetadataName={toMetadataName}
|
||||
recipientAddress={toAddress}
|
||||
recipientEns={toEns}
|
||||
recipientNickname={toNickname}
|
||||
|
@ -5,6 +5,9 @@ import {
|
||||
getIsBuyableChain,
|
||||
getNetworkIdentifier,
|
||||
getSwapsDefaultToken,
|
||||
getMetadataContractName,
|
||||
getAccountName,
|
||||
getMetaMaskIdentities,
|
||||
} from '../../../selectors';
|
||||
import ConfirmPageContainer from './confirm-page-container.component';
|
||||
|
||||
@ -15,11 +18,15 @@ function mapStateToProps(state, ownProps) {
|
||||
const networkIdentifier = getNetworkIdentifier(state);
|
||||
const defaultToken = getSwapsDefaultToken(state);
|
||||
const accountBalance = defaultToken.string;
|
||||
const identities = getMetaMaskIdentities(state);
|
||||
const toName = getAccountName(identities, to);
|
||||
const toMetadataName = getMetadataContractName(state, to);
|
||||
|
||||
return {
|
||||
isBuyableChain,
|
||||
contact,
|
||||
toName: contact?.name || ownProps.toName,
|
||||
toName,
|
||||
toMetadataName,
|
||||
isOwnedAccount: getAccountsWithLabels(state)
|
||||
.map((accountWithLabel) => accountWithLabel.address)
|
||||
.includes(to),
|
||||
|
@ -15,10 +15,15 @@ import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import { useTransactionInsightSnap } from '../../../../hooks/flask/useTransactionInsightSnap';
|
||||
import SnapContentFooter from '../../flask/snap-content-footer/snap-content-footer';
|
||||
import Box from '../../../ui/box/box';
|
||||
import ActionableMessage from '../../../ui/actionable-message/actionable-message';
|
||||
|
||||
export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
|
||||
const t = useI18nContext();
|
||||
const response = useTransactionInsightSnap({
|
||||
const {
|
||||
data: response,
|
||||
error,
|
||||
loading,
|
||||
} = useTransactionInsightSnap({
|
||||
transaction,
|
||||
chainId,
|
||||
snapId: selectedSnap.id,
|
||||
@ -26,8 +31,8 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
|
||||
|
||||
const data = response?.insights;
|
||||
|
||||
const hasNoData = !data || !Object.keys(data).length;
|
||||
|
||||
const hasNoData =
|
||||
!error && (loading || !data || (data && Object.keys(data).length === 0));
|
||||
return (
|
||||
<Box
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
@ -39,13 +44,13 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
|
||||
textAlign={hasNoData && TEXT_ALIGN.CENTER}
|
||||
className="snap-insight"
|
||||
>
|
||||
{data ? (
|
||||
{!loading && !error && (
|
||||
<Box
|
||||
height="full"
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
className="snap-insight__container"
|
||||
>
|
||||
{Object.keys(data).length ? (
|
||||
{data && Object.keys(data).length > 0 ? (
|
||||
<>
|
||||
<Box
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
@ -94,7 +99,29 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{!loading && error && (
|
||||
<Box
|
||||
paddingTop={0}
|
||||
paddingRight={6}
|
||||
paddingBottom={3}
|
||||
paddingLeft={6}
|
||||
className="snap-insight__container__error"
|
||||
>
|
||||
<ActionableMessage
|
||||
message={t('snapsInsightError', [
|
||||
selectedSnap.manifest.proposedName,
|
||||
error.message,
|
||||
])}
|
||||
type="danger"
|
||||
useIcon
|
||||
iconFillColor="var(--color-error-default)"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<>
|
||||
<Preloader size={40} />
|
||||
<Typography
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { I18nContext } from '../../../contexts/i18n';
|
||||
import Box from '../../ui/box';
|
||||
@ -15,6 +16,8 @@ import {
|
||||
JUSTIFY_CONTENT,
|
||||
SIZES,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { getCustomTokenAmount } from '../../../selectors';
|
||||
import { setCustomTokenAmount } from '../../../ducks/app/app';
|
||||
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
|
||||
|
||||
export default function CustomSpendingCap({
|
||||
@ -22,12 +25,17 @@ export default function CustomSpendingCap({
|
||||
currentTokenBalance,
|
||||
dappProposedValue,
|
||||
siteOrigin,
|
||||
onEdit,
|
||||
passTheErrorText,
|
||||
}) {
|
||||
const t = useContext(I18nContext);
|
||||
const [value, setValue] = useState('');
|
||||
const [customSpendingCapText, setCustomSpendingCapText] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const value = useSelector(getCustomTokenAmount);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [showUseDefaultButton, setShowUseDefaultButton] = useState(
|
||||
value !== String(dappProposedValue) && true,
|
||||
);
|
||||
const inputLogicEmptyStateText = t('inputLogicEmptyState');
|
||||
|
||||
const getInputTextLogic = (inputNumber) => {
|
||||
@ -57,6 +65,10 @@ export default function CustomSpendingCap({
|
||||
};
|
||||
};
|
||||
|
||||
const [customSpendingCapText, setCustomSpendingCapText] = useState(
|
||||
getInputTextLogic(value).description,
|
||||
);
|
||||
|
||||
const handleChange = (valueInput) => {
|
||||
let spendingCapError = '';
|
||||
const inputTextLogic = getInputTextLogic(valueInput);
|
||||
@ -71,9 +83,19 @@ export default function CustomSpendingCap({
|
||||
setError('');
|
||||
}
|
||||
|
||||
setValue(valueInput);
|
||||
dispatch(setCustomTokenAmount(String(valueInput)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== String(dappProposedValue)) {
|
||||
setShowUseDefaultButton(true);
|
||||
}
|
||||
}, [value, dappProposedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
passTheErrorText(error);
|
||||
}, [error, passTheErrorText]);
|
||||
|
||||
const chooseTooltipContentText =
|
||||
value > currentTokenBalance
|
||||
? t('warningTooltipText', [
|
||||
@ -100,7 +122,6 @@ export default function CustomSpendingCap({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleChange(currentTokenBalance);
|
||||
setValue(currentTokenBalance);
|
||||
}}
|
||||
>
|
||||
{t('max')}
|
||||
@ -131,6 +152,7 @@ export default function CustomSpendingCap({
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
numeric
|
||||
dataTestId="custom-spending-cap-input"
|
||||
autoFocus
|
||||
wrappingLabelProps={{ as: 'div' }}
|
||||
@ -151,21 +173,19 @@ export default function CustomSpendingCap({
|
||||
error={error}
|
||||
value={value}
|
||||
titleDetail={
|
||||
<button
|
||||
className="custom-spending-cap__input--button"
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (value <= currentTokenBalance || error) {
|
||||
showUseDefaultButton && (
|
||||
<button
|
||||
className="custom-spending-cap__input--button"
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowUseDefaultButton(false);
|
||||
handleChange(dappProposedValue);
|
||||
setValue(dappProposedValue);
|
||||
} else {
|
||||
onEdit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value > currentTokenBalance ? t('edit') : t('useDefault')}
|
||||
</button>
|
||||
}}
|
||||
>
|
||||
{t('useDefault')}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
|
||||
allowDecimals
|
||||
@ -202,7 +222,7 @@ CustomSpendingCap.propTypes = {
|
||||
*/
|
||||
siteOrigin: PropTypes.string,
|
||||
/**
|
||||
* onClick handler for the Edit link
|
||||
* Parent component's callback function passed in order to get the error text
|
||||
*/
|
||||
onEdit: PropTypes.func,
|
||||
passTheErrorText: PropTypes.func,
|
||||
};
|
||||
|
@ -17,8 +17,8 @@ export default {
|
||||
siteOrigin: {
|
||||
control: { type: 'text' },
|
||||
},
|
||||
onEdit: {
|
||||
action: 'onEdit',
|
||||
passTheErrorText: {
|
||||
action: 'passTheErrorText',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
|
@ -21,6 +21,7 @@
|
||||
position: absolute;
|
||||
margin-top: 55px;
|
||||
margin-inline-start: -75px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,4 +29,10 @@
|
||||
color: var(--color-error-default);
|
||||
padding-inline-end: 60px;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']:hover::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -14,77 +14,89 @@ export default function UpdateSnapPermissionList({
|
||||
const t = useI18nContext();
|
||||
|
||||
const ApprovedPermissions = () => {
|
||||
return Object.keys(approvedPermissions).map((approvedPermission) => {
|
||||
const { label, rightIcon } = getPermissionDescription(
|
||||
t,
|
||||
approvedPermission,
|
||||
);
|
||||
const { date } = approvedPermissions[approvedPermission];
|
||||
const formattedDate = formatDate(date, 'yyyy-MM-dd');
|
||||
return (
|
||||
<div className="approved-permission" key={approvedPermission}>
|
||||
<i className="fas fa-check" />
|
||||
<div className="permission-description">
|
||||
{label}
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
className="permission-description-subtext"
|
||||
boxProps={{ paddingTop: 1 }}
|
||||
>
|
||||
{t('approvedOn', [formattedDate])}
|
||||
</Typography>
|
||||
return Object.entries(approvedPermissions).map(
|
||||
([permissionName, permissionValue]) => {
|
||||
const { label, rightIcon } = getPermissionDescription(
|
||||
t,
|
||||
permissionName,
|
||||
permissionValue,
|
||||
);
|
||||
const { date } = permissionValue;
|
||||
const formattedDate = formatDate(date, 'yyyy-MM-dd');
|
||||
return (
|
||||
<div className="approved-permission" key={permissionName}>
|
||||
<i className="fas fa-check" />
|
||||
<div className="permission-description">
|
||||
{label}
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
className="permission-description-subtext"
|
||||
boxProps={{ paddingTop: 1 }}
|
||||
>
|
||||
{t('approvedOn', [formattedDate])}
|
||||
</Typography>
|
||||
</div>
|
||||
{rightIcon && <i className={rightIcon} />}
|
||||
</div>
|
||||
{rightIcon && <i className={rightIcon} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const RevokedPermissions = () => {
|
||||
return Object.keys(revokedPermissions).map((revokedPermission) => {
|
||||
const { label, rightIcon } = getPermissionDescription(
|
||||
t,
|
||||
revokedPermission,
|
||||
);
|
||||
return (
|
||||
<div className="revoked-permission" key={revokedPermission}>
|
||||
<i className="fas fa-x" />
|
||||
<div className="permission-description">
|
||||
{label}
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
boxProps={{ paddingTop: 1 }}
|
||||
className="permission-description-subtext"
|
||||
>
|
||||
{t('permissionRevoked')}
|
||||
</Typography>
|
||||
return Object.entries(revokedPermissions).map(
|
||||
([permissionName, permissionValue]) => {
|
||||
const { label, rightIcon } = getPermissionDescription(
|
||||
t,
|
||||
permissionName,
|
||||
permissionValue,
|
||||
);
|
||||
return (
|
||||
<div className="revoked-permission" key={permissionName}>
|
||||
<i className="fas fa-x" />
|
||||
<div className="permission-description">
|
||||
{label}
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
boxProps={{ paddingTop: 1 }}
|
||||
className="permission-description-subtext"
|
||||
>
|
||||
{t('permissionRevoked')}
|
||||
</Typography>
|
||||
</div>
|
||||
{rightIcon && <i className={rightIcon} />}
|
||||
</div>
|
||||
{rightIcon && <i className={rightIcon} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const NewPermissions = () => {
|
||||
return Object.keys(newPermissions).map((newPermission) => {
|
||||
const { label, rightIcon } = getPermissionDescription(t, newPermission);
|
||||
return (
|
||||
<div className="new-permission" key={newPermission}>
|
||||
<i className="fas fa-arrow-right" />
|
||||
<div className="permission-description">
|
||||
{label}
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
boxProps={{ paddingTop: 1 }}
|
||||
className="permission-description-subtext"
|
||||
>
|
||||
{t('permissionRequested')}
|
||||
</Typography>
|
||||
return Object.entries(newPermissions).map(
|
||||
([permissionName, permissionValue]) => {
|
||||
const { label, rightIcon } = getPermissionDescription(
|
||||
t,
|
||||
permissionName,
|
||||
permissionValue,
|
||||
);
|
||||
return (
|
||||
<div className="new-permission" key={permissionName}>
|
||||
<i className="fas fa-arrow-right" />
|
||||
<div className="permission-description">
|
||||
{label}
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
boxProps={{ paddingTop: 1 }}
|
||||
className="permission-description-subtext"
|
||||
>
|
||||
{t('permissionRequested')}
|
||||
</Typography>
|
||||
</div>
|
||||
{rightIcon && <i className={rightIcon} />}
|
||||
</div>
|
||||
{rightIcon && <i className={rightIcon} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,7 @@
|
||||
@import 'signature-request-footer/index';
|
||||
@import 'signature-request-header/index';
|
||||
@import 'signature-request-message/index';
|
||||
@import 'signature-request-data/index';
|
||||
|
||||
.signature-request {
|
||||
display: flex;
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './signature-request-data';
|
@ -0,0 +1,26 @@
|
||||
.signature-request-data {
|
||||
&__node {
|
||||
&__value {
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
&__address {
|
||||
[dir='rtl'] & {
|
||||
/*rtl:ignore*/
|
||||
direction: ltr;
|
||||
|
||||
/*rtl:ignore*/
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
|
||||
/*rtl:ignore*/
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getMetaMaskIdentities, getAccountName } from '../../../../selectors';
|
||||
import Address from '../../transaction-decoding/components/decoding/address';
|
||||
import {
|
||||
isValidHexAddress,
|
||||
toChecksumHexAddress,
|
||||
} from '../../../../../shared/modules/hexstring-utils';
|
||||
import Box from '../../../ui/box';
|
||||
import Typography from '../../../ui/typography';
|
||||
import {
|
||||
DISPLAY,
|
||||
COLORS,
|
||||
FONT_WEIGHT,
|
||||
TYPOGRAPHY,
|
||||
} from '../../../../helpers/constants/design-system';
|
||||
|
||||
export default function SignatureRequestData({ data }) {
|
||||
const identities = useSelector(getMetaMaskIdentities);
|
||||
|
||||
return (
|
||||
<Box className="signature-request-data__node">
|
||||
{Object.entries(data).map(([label, value], i) => (
|
||||
<Box
|
||||
className="signature-request-data__node"
|
||||
key={i}
|
||||
paddingLeft={2}
|
||||
display={
|
||||
typeof value !== 'object' || value === null ? DISPLAY.FLEX : null
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
as="span"
|
||||
color={COLORS.TEXT_DEFAULT}
|
||||
marginLeft={4}
|
||||
fontWeight={
|
||||
typeof value === 'object' ? FONT_WEIGHT.BOLD : FONT_WEIGHT.NORMAL
|
||||
}
|
||||
>
|
||||
{label.charAt(0).toUpperCase() + label.slice(1)}:{' '}
|
||||
</Typography>
|
||||
{typeof value === 'object' && value !== null ? (
|
||||
<SignatureRequestData data={value} />
|
||||
) : (
|
||||
<Typography
|
||||
as="span"
|
||||
color={COLORS.TEXT_DEFAULT}
|
||||
marginLeft={4}
|
||||
className="signature-request-data__node__value"
|
||||
>
|
||||
{isValidHexAddress(value, {
|
||||
mixedCaseUseChecksum: true,
|
||||
}) ? (
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H7}
|
||||
color={COLORS.INFO_DEFAULT}
|
||||
className="signature-request-data__node__value__address"
|
||||
>
|
||||
<Address
|
||||
addressOnly
|
||||
checksummedRecipientAddress={toChecksumHexAddress(value)}
|
||||
recipientName={getAccountName(identities, value)}
|
||||
/>
|
||||
</Typography>
|
||||
) : (
|
||||
`${value}`
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
SignatureRequestData.propTypes = {
|
||||
data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
|
||||
};
|
@ -1 +1 @@
|
||||
export { default } from './signature-request-message.component';
|
||||
export { default } from './signature-request-message';
|
||||
|
@ -1,75 +1,24 @@
|
||||
.signature-request-message {
|
||||
flex: 1 60%;
|
||||
display: flex;
|
||||
max-height: 231px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&__title {
|
||||
@include H6;
|
||||
|
||||
font-weight: 500;
|
||||
color: var(--color-text-alternative);
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include H6;
|
||||
|
||||
flex: 1 1 0;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
color: var(--color-text-alternative);
|
||||
}
|
||||
|
||||
&--root {
|
||||
&__root {
|
||||
flex: 1 100%;
|
||||
background-color: var(--color-background-alternative);
|
||||
padding-bottom: 0.5rem;
|
||||
overflow: auto;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
|
||||
@include screen-sm-min {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&--node,
|
||||
&--node-leaf {
|
||||
padding-left: 0.3rem;
|
||||
|
||||
&-label {
|
||||
color: var(--color-text-alternative);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: var(--color-text-default);
|
||||
margin-left: 0.5rem;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
&--node-leaf {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__scroll-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--color-border-default);
|
||||
background: var(--color-background-alternative);
|
||||
color: var(--color-icon-default);
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
right: 28px;
|
||||
bottom: 12px;
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class SignatureRequestMessage extends PureComponent {
|
||||
static propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
onMessageScrolled: PropTypes.func,
|
||||
setMessageRootRef: PropTypes.func,
|
||||
messageRootRef: PropTypes.object,
|
||||
messageIsScrollable: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
messageIsScrolled: false,
|
||||
};
|
||||
|
||||
setMessageIsScrolled = () => {
|
||||
if (!this.props.messageRootRef || this.state.messageIsScrolled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, offsetHeight, scrollHeight } = this.props.messageRootRef;
|
||||
const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight;
|
||||
|
||||
if (isAtBottom) {
|
||||
this.setState({ messageIsScrolled: true });
|
||||
this.props.onMessageScrolled();
|
||||
}
|
||||
};
|
||||
|
||||
onScroll = debounce(this.setMessageIsScrolled, 25);
|
||||
|
||||
renderNode(data) {
|
||||
return (
|
||||
<div className="signature-request-message--node">
|
||||
{Object.entries(data).map(([label, value], i) => (
|
||||
<div
|
||||
className={classnames('signature-request-message--node', {
|
||||
'signature-request-message--node-leaf':
|
||||
typeof value !== 'object' || value === null,
|
||||
})}
|
||||
key={i}
|
||||
>
|
||||
<span className="signature-request-message--node-label">
|
||||
{label}:{' '}
|
||||
</span>
|
||||
{typeof value === 'object' && value !== null ? (
|
||||
this.renderNode(value)
|
||||
) : (
|
||||
<span className="signature-request-message--node-value">
|
||||
{`${value}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderScrollButton() {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
this.setState({ messageIsScrolled: true });
|
||||
this.props.onMessageScrolled();
|
||||
this.props.messageRootRef.scrollTo(
|
||||
0,
|
||||
this.props.messageRootRef.scrollHeight,
|
||||
);
|
||||
}}
|
||||
className="signature-request-message__scroll-button"
|
||||
data-testid="signature-request-scroll-button"
|
||||
>
|
||||
<i className="fa fa-arrow-down" title={this.context.t('scrollDown')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, messageIsScrollable } = this.props;
|
||||
|
||||
return (
|
||||
<div onScroll={this.onScroll} className="signature-request-message">
|
||||
{messageIsScrollable ? this.renderScrollButton() : null}
|
||||
<div className="signature-request-message__title">
|
||||
{this.context.t('signatureRequest1')}
|
||||
</div>
|
||||
<div
|
||||
className="signature-request-message--root"
|
||||
ref={this.props.setMessageRootRef}
|
||||
>
|
||||
{this.renderNode(data)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
import { I18nContext } from '../../../../contexts/i18n';
|
||||
import Box from '../../../ui/box';
|
||||
import Typography from '../../../ui/typography';
|
||||
import {
|
||||
DISPLAY,
|
||||
ALIGN_ITEMS,
|
||||
JUSTIFY_CONTENT,
|
||||
COLORS,
|
||||
FONT_WEIGHT,
|
||||
FLEX_DIRECTION,
|
||||
SIZES,
|
||||
} from '../../../../helpers/constants/design-system';
|
||||
import SignatureRequestData from '../signature-request-data';
|
||||
|
||||
export default function SignatureRequestMessage({
|
||||
data,
|
||||
onMessageScrolled,
|
||||
setMessageRootRef,
|
||||
messageRootRef,
|
||||
messageIsScrollable,
|
||||
}) {
|
||||
const t = useContext(I18nContext);
|
||||
const [messageIsScrolled, setMessageIsScrolled] = useState(false);
|
||||
const setMessageIsScrolledAtBottom = () => {
|
||||
if (!messageRootRef || messageIsScrolled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, offsetHeight, scrollHeight } = messageRootRef;
|
||||
const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight;
|
||||
|
||||
if (isAtBottom) {
|
||||
setMessageIsScrolled(true);
|
||||
onMessageScrolled();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
onScroll={debounce(setMessageIsScrolledAtBottom, 25)}
|
||||
className="signature-request-message"
|
||||
>
|
||||
{messageIsScrollable ? (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
borderColor={COLORS.BORDER_DEFAULT}
|
||||
backgroundColor={COLORS.BACKGROUND_DEFAULT}
|
||||
color={COLORS.ICON_DEFAULT}
|
||||
onClick={() => {
|
||||
setMessageIsScrolled(true);
|
||||
onMessageScrolled();
|
||||
messageRootRef?.scrollTo(0, messageRootRef?.scrollHeight);
|
||||
}}
|
||||
className="signature-request-message__scroll-button"
|
||||
data-testid="signature-request-scroll-button"
|
||||
>
|
||||
<i className="fa fa-arrow-down" aria-label={t('scrollDown')} />
|
||||
</Box>
|
||||
) : null}
|
||||
<Box
|
||||
backgroundColor={COLORS.BACKGROUND_DEFAULT}
|
||||
paddingBottom={3}
|
||||
paddingTop={3}
|
||||
paddingRight={3}
|
||||
margin={2}
|
||||
borderRadius={SIZES.XL}
|
||||
borderColor={COLORS.BORDER_MUTED}
|
||||
className="signature-request-message__root"
|
||||
ref={setMessageRootRef}
|
||||
>
|
||||
<Typography
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
color={COLORS.TEXT_DEFAULT}
|
||||
marginLeft={4}
|
||||
className="signature-request-message__title"
|
||||
>
|
||||
{t('signatureRequest1')}
|
||||
</Typography>
|
||||
<SignatureRequestData data={data} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
SignatureRequestMessage.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
onMessageScrolled: PropTypes.func,
|
||||
setMessageRootRef: PropTypes.func,
|
||||
messageRootRef: PropTypes.object,
|
||||
messageIsScrollable: PropTypes.bool,
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import SignatureRequestMessage from './signature-request-message';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/SignatureRequestMessage',
|
||||
id: __filename,
|
||||
component: SignatureRequestMessage,
|
||||
argTypes: {
|
||||
data: { control: 'object' },
|
||||
onMessageScrolled: { action: 'onMessageScrolled' },
|
||||
setMessageRootRef: { action: 'setMessageRootRef' },
|
||||
messageRootRef: { control: 'object' },
|
||||
messageIsScrollable: { control: 'boolean' },
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => {
|
||||
return <SignatureRequestMessage {...args} />;
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
data: JSON.parse(
|
||||
JSON.stringify({
|
||||
domain: {
|
||||
name: 'happydapp.website',
|
||||
},
|
||||
message: {
|
||||
string: 'haay wuurl',
|
||||
number: 42,
|
||||
},
|
||||
primaryType: 'Mail',
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'version', type: 'string' },
|
||||
{ name: 'chainId', type: 'uint256' },
|
||||
{ name: 'verifyingContract', type: 'address' },
|
||||
],
|
||||
Group: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'members', type: 'Person[]' },
|
||||
],
|
||||
Mail: [
|
||||
{ name: 'from', type: 'Person' },
|
||||
{ name: 'to', type: 'Person[]' },
|
||||
{ name: 'contents', type: 'string' },
|
||||
],
|
||||
Person: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'wallets', type: 'address[]' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
messageIsScrollable: true,
|
||||
};
|
@ -8,6 +8,46 @@ import SignatureRequest from './signature-request.container';
|
||||
describe('Signature Request', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
tokenList: {
|
||||
'0x514910771af9ca656af840dff83e8264ecf986ca': {
|
||||
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
||||
symbol: 'LINK',
|
||||
decimals: 18,
|
||||
name: 'ChainLink Token',
|
||||
iconUrl:
|
||||
'https://crypto.com/price/coin-data/icon/LINK/color_icon.png',
|
||||
aggregators: [
|
||||
'Aave',
|
||||
'Bancor',
|
||||
'CMC',
|
||||
'Crypto.com',
|
||||
'CoinGecko',
|
||||
'1inch',
|
||||
'Paraswap',
|
||||
'PMM',
|
||||
'Zapper',
|
||||
'Zerion',
|
||||
'0x',
|
||||
],
|
||||
occurrences: 12,
|
||||
unlisted: false,
|
||||
},
|
||||
},
|
||||
identities: {
|
||||
'0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': {
|
||||
name: 'Account 2',
|
||||
address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e',
|
||||
},
|
||||
},
|
||||
addressBook: {
|
||||
undefined: {
|
||||
0: {
|
||||
address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0',
|
||||
name: '',
|
||||
isEns: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: 'rpc',
|
||||
},
|
||||
|
@ -5,7 +5,10 @@ import copyToClipboard from 'copy-to-clipboard';
|
||||
import { shortenAddress } from '../../../../../../helpers/utils/util';
|
||||
import Identicon from '../../../../../ui/identicon';
|
||||
import { useI18nContext } from '../../../../../../hooks/useI18nContext';
|
||||
import { getAddressBook } from '../../../../../../selectors';
|
||||
import {
|
||||
getMetadataContractName,
|
||||
getAddressBook,
|
||||
} from '../../../../../../selectors';
|
||||
import NicknamePopovers from '../../../../modals/nickname-popovers';
|
||||
|
||||
const Address = ({
|
||||
@ -20,15 +23,25 @@ const Address = ({
|
||||
|
||||
const addressBook = useSelector(getAddressBook);
|
||||
const addressBookEntryObject = addressBook.find(
|
||||
(entry) => entry.address === checksummedRecipientAddress,
|
||||
(entry) =>
|
||||
entry.address.toLowerCase() === checksummedRecipientAddress.toLowerCase(),
|
||||
);
|
||||
const recipientNickname = addressBookEntryObject?.name;
|
||||
const recipientMetadataName = useSelector((state) =>
|
||||
getMetadataContractName(state, checksummedRecipientAddress),
|
||||
);
|
||||
|
||||
const recipientToRender = addressOnly
|
||||
? recipientNickname ||
|
||||
? recipientName ||
|
||||
recipientNickname ||
|
||||
recipientMetadataName ||
|
||||
recipientEns ||
|
||||
shortenAddress(checksummedRecipientAddress)
|
||||
: recipientNickname || recipientEns || recipientName || t('newContract');
|
||||
: recipientName ||
|
||||
recipientNickname ||
|
||||
recipientMetadataName ||
|
||||
recipientEns ||
|
||||
t('newContract');
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -7,6 +7,9 @@
|
||||
|
||||
.tx-insight {
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&-loading {
|
||||
display: flex;
|
||||
@ -167,6 +170,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tx-insight-component-address {
|
||||
&__sender-icon {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,6 +41,8 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
recipientEns: PropTypes.string,
|
||||
recipientAddress: PropTypes.string,
|
||||
recipientName: PropTypes.string,
|
||||
recipientMetadataName: PropTypes.string,
|
||||
rpcPrefs: PropTypes.object,
|
||||
senderAddress: PropTypes.string.isRequired,
|
||||
tryReverseResolveAddress: PropTypes.func.isRequired,
|
||||
@ -139,6 +141,8 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
showRetry,
|
||||
recipientEns,
|
||||
recipientAddress,
|
||||
recipientName,
|
||||
recipientMetadataName,
|
||||
senderAddress,
|
||||
isEarliestNonce,
|
||||
senderNickname,
|
||||
@ -238,6 +242,8 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
recipientEns={recipientEns}
|
||||
recipientAddress={recipientAddress}
|
||||
recipientNickname={recipientNickname}
|
||||
recipientName={recipientName}
|
||||
recipientMetadataName={recipientMetadataName}
|
||||
senderName={senderNickname}
|
||||
senderAddress={senderAddress}
|
||||
onRecipientClick={() => {
|
||||
|
@ -8,6 +8,9 @@ import {
|
||||
getIsCustomNetwork,
|
||||
getRpcPrefsForCurrentProvider,
|
||||
getEnsResolutionByAddress,
|
||||
getAccountName,
|
||||
getMetadataContractName,
|
||||
getMetaMaskIdentities,
|
||||
} from '../../../selectors';
|
||||
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
|
||||
import TransactionListItemDetails from './transaction-list-item-details.component';
|
||||
@ -20,6 +23,12 @@ const mapStateToProps = (state, ownProps) => {
|
||||
recipientEns = getEnsResolutionByAddress(state, address);
|
||||
}
|
||||
const addressBook = getAddressBook(state);
|
||||
const identities = getMetaMaskIdentities(state);
|
||||
const recipientName = getAccountName(identities, recipientAddress);
|
||||
const recipientMetadataName = getMetadataContractName(
|
||||
state,
|
||||
recipientAddress,
|
||||
);
|
||||
|
||||
const getNickName = (address) => {
|
||||
const entry = addressBook.find((contact) => {
|
||||
@ -38,6 +47,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
recipientNickname: recipientAddress ? getNickName(recipientAddress) : null,
|
||||
isCustomNetwork,
|
||||
blockExplorerLinkText: getBlockExplorerLinkText(state),
|
||||
recipientName,
|
||||
recipientMetadataName,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -22,7 +22,7 @@ The `ButtonBase` accepts all props below as well as all [Box](/docs/ui-component
|
||||
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js`
|
||||
to change the size of `ButtonBase`. Defaults to `SIZES.MD`
|
||||
|
||||
Optional: `BUTTON_SIZES` from `./button-base` object can be used instead of `SIZES`.
|
||||
Optional: `BUTTON_BASE_SIZES` from `./button-base` object can be used instead of `SIZES`.
|
||||
|
||||
Possible sizes include:
|
||||
|
||||
@ -86,6 +86,20 @@ import { ButtonBase } from '../ui/component-library';
|
||||
</ButtonBase>
|
||||
```
|
||||
|
||||
### Href
|
||||
|
||||
When an `href` prop is passed it will change the element to an anchor(`a`) tag.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-base-button-base-stories-js--href" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonBase } from '../ui/component-library';
|
||||
|
||||
<ButtonBase href="/metamask">Anchor Element</ButtonBase>;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the boolean `disabled` prop to disable button
|
||||
|
@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ButtonBase should render button element correctly and match snapshot 1`] = `
|
||||
<div>
|
||||
<button
|
||||
class="box mm-button mm-button--size-md box--padding-right-4 box--padding-left-4 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center"
|
||||
data-testid="button-base"
|
||||
>
|
||||
<span
|
||||
class="box text mm-button__content text--body-md text--color-inherit box--gap-2 box--flex-direction-row box--justify-content-center box--align-items-center box--display-flex"
|
||||
>
|
||||
Button base
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
@ -1,6 +1,6 @@
|
||||
import { SIZES } from '../../../helpers/constants/design-system';
|
||||
|
||||
export const BUTTON_SIZES = {
|
||||
export const BUTTON_BASE_SIZES = {
|
||||
SM: SIZES.SM,
|
||||
MD: SIZES.MD,
|
||||
LG: SIZES.LG,
|
@ -15,14 +15,15 @@ import {
|
||||
SIZES,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { BUTTON_SIZES } from './button.constants';
|
||||
import { BUTTON_BASE_SIZES } from './button-base.constants';
|
||||
|
||||
export const ButtonBase = ({
|
||||
as = 'button',
|
||||
block,
|
||||
children,
|
||||
className,
|
||||
size = BUTTON_SIZES.MD,
|
||||
href,
|
||||
size = BUTTON_BASE_SIZES.MD,
|
||||
icon,
|
||||
iconPositionRight,
|
||||
loading,
|
||||
@ -30,12 +31,12 @@ export const ButtonBase = ({
|
||||
iconProps,
|
||||
...props
|
||||
}) => {
|
||||
const Tag = props?.href ? 'a' : as;
|
||||
const Tag = href ? 'a' : as;
|
||||
return (
|
||||
<Box
|
||||
as={Tag}
|
||||
paddingLeft={size === BUTTON_SIZES.AUTO ? 0 : 4}
|
||||
paddingRight={size === BUTTON_SIZES.AUTO ? 0 : 4}
|
||||
paddingLeft={size === BUTTON_BASE_SIZES.AUTO ? 0 : 4}
|
||||
paddingRight={size === BUTTON_BASE_SIZES.AUTO ? 0 : 4}
|
||||
className={classnames(
|
||||
'mm-button',
|
||||
`mm-button--size-${size}`,
|
||||
@ -61,13 +62,13 @@ export const ButtonBase = ({
|
||||
iconPositionRight ? FLEX_DIRECTION.ROW_REVERSE : FLEX_DIRECTION.ROW
|
||||
}
|
||||
gap={2}
|
||||
variant={size === BUTTON_SIZES.AUTO ? TEXT.INHERIT : TEXT.BODY_MD}
|
||||
variant={size === BUTTON_BASE_SIZES.AUTO ? TEXT.INHERIT : TEXT.BODY_MD}
|
||||
color={TEXT_COLORS.INHERIT}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
name={icon}
|
||||
size={size === BUTTON_SIZES.AUTO ? SIZES.AUTO : SIZES.SM}
|
||||
size={size === BUTTON_BASE_SIZES.AUTO ? SIZES.AUTO : SIZES.SM}
|
||||
{...iconProps}
|
||||
/>
|
||||
)}
|
||||
@ -77,7 +78,7 @@ export const ButtonBase = ({
|
||||
<Icon
|
||||
className="mm-button__icon-loading"
|
||||
name={ICON_NAMES.LOADING_FILLED}
|
||||
size={size === BUTTON_SIZES.AUTO ? SIZES.AUTO : SIZES.MD}
|
||||
size={size === BUTTON_BASE_SIZES.AUTO ? SIZES.AUTO : SIZES.MD}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@ -105,6 +106,10 @@ ButtonBase.propTypes = {
|
||||
* Boolean to disable button
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag
|
||||
*/
|
||||
href: PropTypes.string,
|
||||
/**
|
||||
* Add icon to left side of button text passing icon name
|
||||
* The name of the icon to display. Should be one of ICON_NAMES
|
||||
@ -125,9 +130,9 @@ ButtonBase.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
/**
|
||||
* The size of the ButtonBase.
|
||||
* Possible values could be 'SIZES.AUTO', 'SIZES.SM', 'SIZES.MD', 'SIZES.LG',
|
||||
* Possible values could be 'SIZES.AUTO', 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px),
|
||||
*/
|
||||
size: PropTypes.oneOf(Object.values(BUTTON_SIZES)),
|
||||
size: PropTypes.oneOf(Object.values(BUTTON_BASE_SIZES)),
|
||||
/**
|
||||
* Addition style properties to apply to the button.
|
||||
*/
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import Box from '../../ui/box/box';
|
||||
import { ICON_NAMES } from '../icon';
|
||||
import { Text } from '../text';
|
||||
import { BUTTON_SIZES } from './button.constants';
|
||||
import { BUTTON_BASE_SIZES } from './button-base.constants';
|
||||
import { ButtonBase } from './button-base';
|
||||
import README from './README.mdx';
|
||||
|
||||
@ -66,7 +66,7 @@ export default {
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: Object.values(BUTTON_SIZES),
|
||||
options: Object.values(BUTTON_BASE_SIZES),
|
||||
},
|
||||
marginTop: {
|
||||
options: marginSizeControlOptions,
|
||||
@ -145,6 +145,12 @@ export const As = (args) => (
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Href = (args) => <ButtonBase {...args}>Anchor Element</ButtonBase>;
|
||||
|
||||
Href.args = {
|
||||
href: '/metamask',
|
||||
};
|
||||
|
||||
export const Disabled = (args) => (
|
||||
<ButtonBase {...args}>Disabled Button</ButtonBase>
|
||||
);
|
||||
|
@ -1,17 +1,18 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { BUTTON_SIZES } from './button.constants';
|
||||
import { BUTTON_BASE_SIZES } from './button-base.constants';
|
||||
import { ButtonBase } from './button-base';
|
||||
|
||||
describe('ButtonBase', () => {
|
||||
it('should render button element correctly', () => {
|
||||
it('should render button element correctly and match snapshot', () => {
|
||||
const { getByTestId, getByText, container } = render(
|
||||
<ButtonBase data-testid="button-base">Button base</ButtonBase>,
|
||||
);
|
||||
expect(getByText('Button base')).toBeDefined();
|
||||
expect(container.querySelector('button')).toBeDefined();
|
||||
expect(getByTestId('button-base')).toHaveClass('mm-button');
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render anchor element correctly', () => {
|
||||
@ -40,23 +41,35 @@ describe('ButtonBase', () => {
|
||||
it('should render with different size classes', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<ButtonBase size={BUTTON_SIZES.AUTO} data-testid={BUTTON_SIZES.AUTO} />
|
||||
<ButtonBase size={BUTTON_SIZES.SM} data-testid={BUTTON_SIZES.SM} />
|
||||
<ButtonBase size={BUTTON_SIZES.MD} data-testid={BUTTON_SIZES.MD} />
|
||||
<ButtonBase size={BUTTON_SIZES.LG} data-testid={BUTTON_SIZES.LG} />
|
||||
<ButtonBase
|
||||
size={BUTTON_BASE_SIZES.AUTO}
|
||||
data-testid={BUTTON_BASE_SIZES.AUTO}
|
||||
/>
|
||||
<ButtonBase
|
||||
size={BUTTON_BASE_SIZES.SM}
|
||||
data-testid={BUTTON_BASE_SIZES.SM}
|
||||
/>
|
||||
<ButtonBase
|
||||
size={BUTTON_BASE_SIZES.MD}
|
||||
data-testid={BUTTON_BASE_SIZES.MD}
|
||||
/>
|
||||
<ButtonBase
|
||||
size={BUTTON_BASE_SIZES.LG}
|
||||
data-testid={BUTTON_BASE_SIZES.LG}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId(BUTTON_SIZES.AUTO)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_SIZES.AUTO}`,
|
||||
expect(getByTestId(BUTTON_BASE_SIZES.AUTO)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_BASE_SIZES.AUTO}`,
|
||||
);
|
||||
expect(getByTestId(BUTTON_SIZES.SM)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_SIZES.SM}`,
|
||||
expect(getByTestId(BUTTON_BASE_SIZES.SM)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_BASE_SIZES.SM}`,
|
||||
);
|
||||
expect(getByTestId(BUTTON_SIZES.MD)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_SIZES.MD}`,
|
||||
expect(getByTestId(BUTTON_BASE_SIZES.MD)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_BASE_SIZES.MD}`,
|
||||
);
|
||||
expect(getByTestId(BUTTON_SIZES.LG)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_SIZES.LG}`,
|
||||
expect(getByTestId(BUTTON_BASE_SIZES.LG)).toHaveClass(
|
||||
`mm-button--size-${BUTTON_BASE_SIZES.LG}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
export { ButtonBase } from './button-base';
|
||||
export { BUTTON_SIZES } from './button.constants';
|
||||
export { BUTTON_BASE_SIZES } from './button-base.constants';
|
||||
|
144
ui/components/component-library/button-icon/README.mdx
Normal file
144
ui/components/component-library/button-icon/README.mdx
Normal file
@ -0,0 +1,144 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
import { ButtonIcon } from './button-icon';
|
||||
|
||||
# ButtonIcon
|
||||
|
||||
The `ButtonIcon` is used for icons associated with a user action.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--default-story" />
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
The `ButtonIcon` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props
|
||||
|
||||
<ArgsTable of={ButtonIcon} />
|
||||
|
||||
### Icon<span style={{ color: 'red' }}>\*</span>
|
||||
|
||||
Use the required `icon` prop with `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--icon" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
import { ICON_NAMES } from '../icon';
|
||||
|
||||
<ButtonIcon icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close" />;
|
||||
```
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js`
|
||||
to change the size of `ButtonIcon`. Defaults to `SIZES.SM`
|
||||
|
||||
Optional: `BUTTON_ICON_SIZES` from `./button-icon` object can be used instead of `SIZES`.
|
||||
|
||||
Possible sizes include:
|
||||
|
||||
- `SIZES.SM` 24px
|
||||
- `SIZES.LG` 32px
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--size" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { SIZES } from '../../../helpers/constants/design-system';
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
|
||||
<ButtonIcon size={SIZES.SM} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
|
||||
<ButtonIcon size={SIZES.LG} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
|
||||
```
|
||||
|
||||
### Aria Label
|
||||
|
||||
Use the `ariaLabel` prop to set the name of the ButtonIcon for proper accessibility
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--aria-label" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
|
||||
|
||||
<ButtonIcon as="button" icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
|
||||
<ButtonIcon as="a" href="https://metamask.io/" target="_blank" icon={ICON_NAMES.EXPORT} color={COLORS.PRIMARY_DEFAULT} ariaLabel="Visit MetaMask.io"/>
|
||||
```
|
||||
|
||||
### As
|
||||
|
||||
Use the `as` box prop to change the element of `ButtonIcon`. Defaults to `button`.
|
||||
|
||||
Button `as` options:
|
||||
|
||||
- `button`
|
||||
- `a`
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--as" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
|
||||
|
||||
<ButtonIcon as="button" icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
|
||||
<ButtonIcon as="a" href="https://metamask.io/" target="_blank" icon={ICON_NAMES.EXPORT} color={COLORS.PRIMARY_DEFAULT} ariaLabel="Visit MetaMask.io"/>
|
||||
```
|
||||
|
||||
### Href
|
||||
|
||||
When an `href` prop is passed it will change the element to an anchor(`a`) tag.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--href" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
|
||||
<ButtonIcon
|
||||
href="https://metamask.io/"
|
||||
target="_blank"
|
||||
icon={ICON_NAMES.EXPORT}
|
||||
color={COLORS.PRIMARY_DEFAULT}
|
||||
ariaLabel="Visit MetaMask.io"
|
||||
/>;
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
Use the `color` prop and the `COLORS` object to change the color of the `ButtonIcon`. Defaults to `COLORS.ICON_DEFAULT`.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--color" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
|
||||
<ButtonIcon
|
||||
icon={ICON_NAMES.EXPORT}
|
||||
color={COLORS.PRIMARY_DEFAULT}
|
||||
ariaLabel="Visit MetaMask.io"
|
||||
/>;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the boolean `disabled` prop to disable button
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--disabled" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { ButtonIcon } from '../ui/component-library';
|
||||
|
||||
<ButtonIcon icon={ICON_NAMES.CLOSE_OUTLINE} disabled ariaLabel="Close" />;
|
||||
```
|
@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ButtonIcon should render button element correctly 1`] = `
|
||||
<div>
|
||||
<button
|
||||
aria-label="add"
|
||||
class="box mm-button-icon mm-button-icon--size-lg box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-icon-default box--background-color-transparent box--rounded-lg"
|
||||
data-testid="button-icon"
|
||||
>
|
||||
<div
|
||||
class="box icon icon--size-lg box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/icon-add-square-filled.svg;"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,6 @@
|
||||
import { SIZES } from '../../../helpers/constants/design-system';
|
||||
|
||||
export const BUTTON_ICON_SIZES = {
|
||||
SM: SIZES.SM,
|
||||
LG: SIZES.LG,
|
||||
};
|
101
ui/components/component-library/button-icon/button-icon.js
Normal file
101
ui/components/component-library/button-icon/button-icon.js
Normal file
@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import {
|
||||
ALIGN_ITEMS,
|
||||
BORDER_RADIUS,
|
||||
COLORS,
|
||||
DISPLAY,
|
||||
JUSTIFY_CONTENT,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
import { BUTTON_ICON_SIZES } from './button-icon.constants';
|
||||
|
||||
export const ButtonIcon = ({
|
||||
ariaLabel,
|
||||
as = 'button',
|
||||
className,
|
||||
color = COLORS.ICON_DEFAULT,
|
||||
href,
|
||||
size = BUTTON_ICON_SIZES.LG,
|
||||
icon,
|
||||
disabled,
|
||||
iconProps,
|
||||
...props
|
||||
}) => {
|
||||
const Tag = href ? 'a' : as;
|
||||
return (
|
||||
<Box
|
||||
aria-label={ariaLabel}
|
||||
as={Tag}
|
||||
className={classnames(
|
||||
'mm-button-icon',
|
||||
`mm-button-icon--size-${size}`,
|
||||
{
|
||||
'mm-button-icon--disabled': disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
color={color}
|
||||
disabled={disabled}
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
borderRadius={BORDER_RADIUS.LG}
|
||||
backgroundColor={COLORS.TRANSPARENT}
|
||||
href={href}
|
||||
{...props}
|
||||
>
|
||||
<Icon name={icon} size={size} {...iconProps} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonIcon.propTypes = {
|
||||
/**
|
||||
* String that adds an accessible name for ButtonIcon
|
||||
*/
|
||||
ariaLabel: PropTypes.string.isRequired,
|
||||
/**
|
||||
* The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag
|
||||
*/
|
||||
as: PropTypes.string,
|
||||
/**
|
||||
* An additional className to apply to the ButtonIcon.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* The color of the ButtonIcon component should use the COLOR object from
|
||||
* ./ui/helpers/constants/design-system.js
|
||||
*/
|
||||
color: PropTypes.oneOf(Object.values(COLORS)),
|
||||
/**
|
||||
* Boolean to disable button
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* When an `href` prop is passed, ButtonIcon will automatically change the root element to be an `a` (anchor) tag
|
||||
*/
|
||||
href: PropTypes.string,
|
||||
/**
|
||||
* The name of the icon to display. Should be one of ICON_NAMES
|
||||
*/
|
||||
icon: PropTypes.string.isRequired, // Can't set PropTypes.oneOf(ICON_NAMES) because ICON_NAMES is an environment variable
|
||||
/**
|
||||
* iconProps accepts all the props from Icon
|
||||
*/
|
||||
iconProps: PropTypes.object,
|
||||
/**
|
||||
* The size of the ButtonIcon.
|
||||
* Possible values could be 'SIZES.SM', 'SIZES.LG',
|
||||
*/
|
||||
size: PropTypes.oneOf(Object.values(BUTTON_ICON_SIZES)),
|
||||
/**
|
||||
* ButtonIcon accepts all the props from Box
|
||||
*/
|
||||
...Box.propTypes,
|
||||
};
|
32
ui/components/component-library/button-icon/button-icon.scss
Normal file
32
ui/components/component-library/button-icon/button-icon.scss
Normal file
@ -0,0 +1,32 @@
|
||||
.mm-button-icon {
|
||||
--button-icon-size: var(--size, 24px);
|
||||
--button-icon-opacity-hover: 0.5; // TODO: replace with design tokens
|
||||
--button-icon-opacity-disabled: 0.3; // TODO: replace with design tokens
|
||||
|
||||
height: var(--button-icon-size);
|
||||
width: var(--button-icon-size);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
// ButtonIcon default states
|
||||
&:active,
|
||||
&:hover {
|
||||
opacity: var(--button-icon-opacity-hover);
|
||||
}
|
||||
|
||||
&--disabled,
|
||||
&:disabled {
|
||||
opacity: var(--button-icon-opacity-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
// ButtonIcon Sizes
|
||||
&--size-sm {
|
||||
--button-icon-size: 24px;
|
||||
}
|
||||
|
||||
&--size-lg {
|
||||
--button-icon-size: 32px;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,186 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ALIGN_ITEMS,
|
||||
COLORS,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
SIZES,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Box from '../../ui/box/box';
|
||||
import { ICON_NAMES } from '../icon';
|
||||
import { BUTTON_ICON_SIZES } from './button-icon.constants';
|
||||
import { ButtonIcon } from './button-icon';
|
||||
import README from './README.mdx';
|
||||
|
||||
const marginSizeControlOptions = [
|
||||
undefined,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
'auto',
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'Components/ComponentLibrary/ButtonIcon',
|
||||
id: __filename,
|
||||
component: ButtonIcon,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: README,
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
ariaLabel: {
|
||||
control: 'text',
|
||||
},
|
||||
as: {
|
||||
control: 'select',
|
||||
options: ['button', 'a'],
|
||||
},
|
||||
className: {
|
||||
control: 'text',
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: Object.values(COLORS),
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
href: {
|
||||
control: 'string',
|
||||
},
|
||||
icon: {
|
||||
control: 'select',
|
||||
options: Object.values(ICON_NAMES),
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: Object.values(BUTTON_ICON_SIZES),
|
||||
},
|
||||
marginTop: {
|
||||
options: marginSizeControlOptions,
|
||||
control: 'select',
|
||||
table: { category: 'box props' },
|
||||
},
|
||||
marginRight: {
|
||||
options: marginSizeControlOptions,
|
||||
control: 'select',
|
||||
table: { category: 'box props' },
|
||||
},
|
||||
marginBottom: {
|
||||
options: marginSizeControlOptions,
|
||||
control: 'select',
|
||||
table: { category: 'box props' },
|
||||
},
|
||||
marginLeft: {
|
||||
options: marginSizeControlOptions,
|
||||
control: 'select',
|
||||
table: { category: 'box props' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <ButtonIcon {...args} />;
|
||||
|
||||
DefaultStory.args = {
|
||||
icon: ICON_NAMES.CLOSE_OUTLINE,
|
||||
ariaLabel: 'Close',
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
export const Icon = (args) => (
|
||||
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close" />
|
||||
);
|
||||
|
||||
export const Size = (args) => (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={ALIGN_ITEMS.BASELINE}
|
||||
gap={1}
|
||||
marginBottom={2}
|
||||
>
|
||||
<ButtonIcon
|
||||
{...args}
|
||||
size={SIZES.SM}
|
||||
icon={ICON_NAMES.CLOSE_OUTLINE}
|
||||
ariaLabel="Close"
|
||||
/>
|
||||
<ButtonIcon
|
||||
{...args}
|
||||
size={SIZES.LG}
|
||||
color={COLORS.PRIMARY}
|
||||
icon={ICON_NAMES.CLOSE_OUTLINE}
|
||||
ariaLabel="Close"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const AriaLabel = (args) => (
|
||||
<>
|
||||
<ButtonIcon
|
||||
as="button"
|
||||
icon={ICON_NAMES.CLOSE_OUTLINE}
|
||||
ariaLabel="Close"
|
||||
{...args}
|
||||
/>
|
||||
<ButtonIcon
|
||||
as="a"
|
||||
href="https://metamask.io/"
|
||||
target="_blank"
|
||||
color={COLORS.PRIMARY_DEFAULT}
|
||||
icon={ICON_NAMES.EXPORT}
|
||||
ariaLabel="Visit MetaMask.io"
|
||||
{...args}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export const As = (args) => (
|
||||
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} gap={2}>
|
||||
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
|
||||
<ButtonIcon
|
||||
as="a"
|
||||
href="#"
|
||||
{...args}
|
||||
color={COLORS.PRIMARY_DEFAULT}
|
||||
icon={ICON_NAMES.EXPORT}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Href = (args) => (
|
||||
<ButtonIcon icon={ICON_NAMES.EXPORT} {...args} target="_blank" />
|
||||
);
|
||||
|
||||
Href.args = {
|
||||
href: 'https://metamask.io/',
|
||||
color: COLORS.PRIMARY_DEFAULT,
|
||||
};
|
||||
|
||||
export const Color = (args) => (
|
||||
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
|
||||
);
|
||||
|
||||
Color.args = {
|
||||
color: COLORS.PRIMARY_DEFAULT,
|
||||
};
|
||||
|
||||
export const Disabled = (args) => (
|
||||
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
|
||||
);
|
||||
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
};
|
146
ui/components/component-library/button-icon/button-icon.test.js
Normal file
146
ui/components/component-library/button-icon/button-icon.test.js
Normal file
@ -0,0 +1,146 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { COLORS } from '../../../helpers/constants/design-system';
|
||||
import { BUTTON_ICON_SIZES } from './button-icon.constants';
|
||||
import { ButtonIcon } from './button-icon';
|
||||
|
||||
describe('ButtonIcon', () => {
|
||||
it('should render button element correctly', () => {
|
||||
const { getByTestId, container } = render(
|
||||
<ButtonIcon
|
||||
data-testid="button-icon"
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('button')).toBeDefined();
|
||||
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render anchor element correctly', () => {
|
||||
const { getByTestId, container } = render(
|
||||
<ButtonIcon
|
||||
as="a"
|
||||
data-testid="button-icon"
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
|
||||
const anchor = container.getElementsByTagName('a').length;
|
||||
expect(anchor).toBe(1);
|
||||
});
|
||||
|
||||
it('should render anchor element correctly using href', () => {
|
||||
const { getByTestId, getByRole } = render(
|
||||
<ButtonIcon
|
||||
href="/metamask"
|
||||
data-testid="button-icon"
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
|
||||
expect(getByRole('link')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render with different size classes', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<ButtonIcon
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
size={BUTTON_ICON_SIZES.SM}
|
||||
data-testid={BUTTON_ICON_SIZES.SM}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
size={BUTTON_ICON_SIZES.LG}
|
||||
data-testid={BUTTON_ICON_SIZES.LG}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId(BUTTON_ICON_SIZES.SM)).toHaveClass(
|
||||
`mm-button-icon--size-${BUTTON_ICON_SIZES.SM}`,
|
||||
);
|
||||
expect(getByTestId(BUTTON_ICON_SIZES.LG)).toHaveClass(
|
||||
`mm-button-icon--size-${BUTTON_ICON_SIZES.LG}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with different colors', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<ButtonIcon
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
color={COLORS.ICON_DEFAULT}
|
||||
data-testid={COLORS.ICON_DEFAULT}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
color={COLORS.ERROR_DEFAULT}
|
||||
data-testid={COLORS.ERROR_DEFAULT}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId(COLORS.ICON_DEFAULT)).toHaveClass(
|
||||
`box--color-${COLORS.ICON_DEFAULT}`,
|
||||
);
|
||||
expect(getByTestId(COLORS.ERROR_DEFAULT)).toHaveClass(
|
||||
`box--color-${COLORS.ERROR_DEFAULT}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with added classname', () => {
|
||||
const { getByTestId } = render(
|
||||
<ButtonIcon
|
||||
data-testid="classname"
|
||||
className="mm-button-icon--test"
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('classname')).toHaveClass('mm-button-icon--test');
|
||||
});
|
||||
|
||||
it('should render with different button states', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<ButtonIcon
|
||||
disabled
|
||||
data-testid="disabled"
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(getByTestId('disabled')).toHaveClass(`mm-button-icon--disabled`);
|
||||
expect(getByTestId('disabled')).toBeDisabled();
|
||||
});
|
||||
it('should render with icon', () => {
|
||||
const { getByTestId } = render(
|
||||
<ButtonIcon
|
||||
data-testid="icon"
|
||||
icon="add-square-filled"
|
||||
ariaLabel="add"
|
||||
iconProps={{ 'data-testid': 'button-icon' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('button-icon')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render with aria-label', () => {
|
||||
const { getByLabelText } = render(
|
||||
<ButtonIcon icon="add-square-filled" ariaLabel="add" />,
|
||||
);
|
||||
|
||||
expect(getByLabelText('add')).toBeDefined();
|
||||
});
|
||||
});
|
2
ui/components/component-library/button-icon/index.js
Normal file
2
ui/components/component-library/button-icon/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { ButtonIcon } from './button-icon';
|
||||
export { BUTTON_ICON_SIZES } from './button-icon.constants';
|
@ -43,12 +43,12 @@ import { ButtonLink } from '../ui/component-library/button/button-link/button-li
|
||||
<ButtonLink size={SIZES.LG} />
|
||||
```
|
||||
|
||||
### Type
|
||||
### Danger
|
||||
|
||||
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonLink`.
|
||||
Use the `danger` boolean prop to change the `ButtonPrimary` to danger color.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-link-button-link-stories-js--type" />
|
||||
<Story id="ui-components-component-library-button-link-button-link-stories-js--danger" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
|
@ -139,7 +139,7 @@ export const Size = (args) => (
|
||||
</>
|
||||
);
|
||||
|
||||
export const Type = (args) => (
|
||||
export const Danger = (args) => (
|
||||
<Box display={DISPLAY.FLEX} gap={1}>
|
||||
<ButtonLink {...args}>Normal</ButtonLink>
|
||||
{/* Test Anchor tag to match exactly as button */}
|
||||
|
@ -53,7 +53,7 @@ describe('ButtonLink', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with different types', () => {
|
||||
it('should render as danger', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<ButtonLink danger data-testid="danger" />
|
||||
|
@ -41,12 +41,12 @@ import { ButtonPrimary } from '../ui/component-library/button/button-primary/but
|
||||
<ButtonPrimary size={SIZES.LG} />
|
||||
```
|
||||
|
||||
### Type
|
||||
### Danger
|
||||
|
||||
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonPrimary`.
|
||||
Use the `danger` boolean prop to change the `ButtonPrimary` to danger color.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--type" />
|
||||
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--danger" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
|
@ -28,7 +28,7 @@ ButtonPrimary.propTypes = {
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Boolean to change button type to Danger when true
|
||||
* When true, `ButtonPrimary` color becomes Danger.
|
||||
*/
|
||||
danger: PropTypes.bool,
|
||||
/**
|
||||
|
@ -122,7 +122,7 @@ export const Size = (args) => (
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Type = (args) => (
|
||||
export const Danger = (args) => (
|
||||
<Box display={DISPLAY.FLEX} gap={1}>
|
||||
<ButtonPrimary {...args}>Normal</ButtonPrimary>
|
||||
{/* Test Anchor tag to match exactly as button */}
|
||||
|
@ -66,7 +66,7 @@ describe('ButtonPrimary', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with different types', () => {
|
||||
it('should render as danger', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<ButtonPrimary danger data-testid="danger" />
|
||||
|
@ -41,12 +41,12 @@ import { ButtonSecondary } from '../ui/component-library/button/button-secondary
|
||||
<ButtonSecondary size={SIZES.LG} />
|
||||
```
|
||||
|
||||
### Type
|
||||
### Danger
|
||||
|
||||
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonSecondary`.
|
||||
Use the `danger` boolean prop to change the `ButtonPrimary` to danger color.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-component-library-button-secondary-button-secondary-stories-js--type" />
|
||||
<Story id="ui-components-component-library-button-secondary-button-secondary-stories-js--danger" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user