diff --git a/.storybook/preview.js b/.storybook/preview.js index 1095bf319496..b46c91339273 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -18,7 +18,6 @@ import { createBrowserHistory } from 'history'; import { setBackgroundConnection } from '../ui/store/background-connection'; import { metamaskStorybookTheme } from './metamask-storybook-theme'; import { DocsContainer } from '@storybook/addon-docs'; -import { useDarkMode } from 'storybook-dark-mode'; import { themes } from '@storybook/theming'; import { AlertMetricsProvider } from '../ui/components/app/alert-system/contexts/alertMetricsContext'; @@ -36,7 +35,13 @@ export const parameters = { }, docs: { container: (context) => { - const isDark = useDarkMode(); + const theme = context?.globals?.theme || 'both'; + const systemPrefersDark = window.matchMedia( + '(prefers-color-scheme: dark)', + ).matches; + + const isDark = + theme === 'dark' || (theme === 'both' && systemPrefersDark); const props = { ...context, @@ -82,6 +87,19 @@ export const globalTypes = { }), }, }, + theme: { + name: 'Color Theme', + description: 'The color theme for the component', + defaultValue: 'both', + toolbar: { + items: [ + { value: 'light', title: 'Light', icon: 'sun' }, + { value: 'dark', title: 'Dark', icon: 'moon' }, + { value: 'both', title: 'Light/Dark', icon: 'paintbrush' }, + ], + dynamicTitle: true, + }, + }, }; export const getNewState = (state, props) => { @@ -104,7 +122,13 @@ const proxiedBackground = new Proxy( setBackgroundConnection(proxiedBackground); const metamaskDecorator = (story, context) => { - const isDark = useDarkMode(); + const { theme } = context.globals; + const systemPrefersDark = window.matchMedia( + '(prefers-color-scheme: dark)', + ).matches; + + const isDark = theme === 'dark' || (theme === 'both' && systemPrefersDark); + const currentLocale = context.globals.locale; const current = allLocales[currentLocale]; @@ -146,4 +170,55 @@ const metamaskDecorator = (story, context) => { ); }; -export const decorators = [metamaskDecorator]; +// Add the withColorScheme decorator +const withColorScheme = (Story, context) => { + const { theme } = context.globals; + const systemPrefersDark = window.matchMedia( + '(prefers-color-scheme: dark)', + ).matches; + + const isDark = theme === 'dark' || (theme === 'both' && systemPrefersDark); + + function Wrapper(props) { + return ( +
+ ); + } + + if (theme === 'light') { + return ( + + + + ); + } + + if (theme === 'dark') { + return ( + + + + ); + } + + return ( +
+ + + + + + +
+ ); +}; + +export const decorators = [metamaskDecorator, withColorScheme]; diff --git a/CHANGELOG.md b/CHANGELOG.md index 058856f36ae2..b6d7f827ca85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.2.2] +### Fixed +- This build was needed to fix release publishing on our master branch. It also includes the addition of the missing v12.2.0 changelog. Functionality and code is equivalent to v12.2.0. + +## [12.2.1] +### Fixed +- This build was needed to fix release publishing on our master branch. It also includes the addition of the missing v12.2.0 changelog. Functionality and code is equivalent to v12.2.0. + +## [12.2.0] +### Added +- Enabled the redesigned SIWE (Sign-In with Ethereum) signature pages ([#25660](https://github.com/MetaMask/metamask-extension/pull/25660)) +- Added support for security alerts on zkSync, Berachain, Scroll, and Metachain One networks ([#25555](https://github.com/MetaMask/metamask-extension/pull/25555)) +- Added an account mismatch warning alert to the SIWE redesign page ([#25613](https://github.com/MetaMask/metamask-extension/pull/25613)) + +### Changed +- Improved the display of large and small token values on the permit signature page ([#25438](https://github.com/MetaMask/metamask-extension/pull/25438)) +- Removed the modals prompting users to enable token and NFT detection ([#26403](https://github.com/MetaMask/metamask-extension/pull/26403)) +- Enabled the redesigned confirmations by default ([#25769](https://github.com/MetaMask/metamask-extension/pull/25769)) +- Improved error messaging during Ledger pairing to guide users when the device is locked or the Ethereum app is not open ([#25462](https://github.com/MetaMask/metamask-extension/pull/25462)) + +### Fixed +- Fixed an issue where removing non-EVM accounts was broken if there was an existing EVM dapp permission ([#25739](https://github.com/MetaMask/metamask-extension/pull/25739)) +- Fixed the issue to show the connected toast only for EVM accounts, hiding it for non-EVM accounts ([#25628](https://github.com/MetaMask/metamask-extension/pull/25628)) +- Fixed an issue where the connected account was missing on the connection page ([#25500](https://github.com/MetaMask/metamask-extension/pull/25500)) +- Fixed an issue where the account name was out of sync in the account list during the connect account flow ([#26542](https://github.com/MetaMask/metamask-extension/pull/26542)) +- Fixed the display of decimal places for token values on permit pages ([#25410](https://github.com/MetaMask/metamask-extension/pull/25410)) +- Fixed the page width for the send page in fullscreen mode ([#25639](https://github.com/MetaMask/metamask-extension/pull/25639)) +- Updated the accounts mismatch banner on the signature page to the new design ([#25626](https://github.com/MetaMask/metamask-extension/pull/25626)) +- Fixed the alignment of the install origin text in the expanded authorship view for Snaps ([#25583](https://github.com/MetaMask/metamask-extension/pull/25583)) + +## [12.1.3] +### Fixed +- Fix `eth_signTypedData` error when `verifyingContract` is not provided ([#26914](https://github.com/MetaMask/metamask-extension/pull/26914)) + ## [12.1.2] ### Fixed - Fix Trezor signing and connecting accounts ([#26882](https://github.com/MetaMask/metamask-extension/pull/26882)) @@ -5010,7 +5044,11 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.1.2...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.2.2...HEAD +[12.2.2]: https://github.com/MetaMask/metamask-extension/compare/v12.2.1...v12.2.2 +[12.2.1]: https://github.com/MetaMask/metamask-extension/compare/v12.2.0...v12.2.1 +[12.2.0]: https://github.com/MetaMask/metamask-extension/compare/v12.1.3...v12.2.0 +[12.1.3]: https://github.com/MetaMask/metamask-extension/compare/v12.1.2...v12.1.3 [12.1.2]: https://github.com/MetaMask/metamask-extension/compare/v12.1.1...v12.1.2 [12.1.1]: https://github.com/MetaMask/metamask-extension/compare/v12.1.0...v12.1.1 [12.1.0]: https://github.com/MetaMask/metamask-extension/compare/v12.0.6...v12.1.0 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 7be4ca71e56d..ff0773178691 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Bestätigen Sie diese Nachricht nur, wenn Sie dem Inhalt zustimmen und der anfragenden Website vertrauen." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Antrag auf Ausgabenobergrenze" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index e9c4753a54cb..3a191b6e9579 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Επιβεβαιώστε αυτό το μήνυμα μόνο εάν εγκρίνετε το περιεχόμενο και εμπιστεύεστε τον ιστότοπο που το ζητάει." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Αίτημα ανώτατου ορίου δαπανών" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ccc7f1acd1d7..5aee8cd393aa 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1075,7 +1075,7 @@ "confirmTitleDescSignature": { "message": "Only confirm this message if you approve the content and trust the requesting site." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Spending cap request" }, "confirmTitleSIWESignature": { @@ -1785,6 +1785,15 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Edit speed up gas fee" }, + "editSpendingCap": { + "message": "Edit spending cap" + }, + "editSpendingCapAccountBalance": { + "message": "Account balance: $1 $2" + }, + "editSpendingCapDesc": { + "message": "Enter the amount that you feel comfortable being spent on your behalf." + }, "enable": { "message": "Enable" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 7b76c55d0d02..e67b9f0e4fcb 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1039,7 +1039,7 @@ "confirmTitleDescSignature": { "message": "Only confirm this message if you approve the content and trust the requesting site." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Spending cap request" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 9bdbe804c65c..88524f86f3cc 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1024,7 +1024,7 @@ "confirmTitleDescSignature": { "message": "Solo confirme este mensaje si aprueba el contenido y confía en el sitio solicitante." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Solicitud de límite de gasto" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 9fa069312aea..f376101c5b2d 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Ne confirmez ce message que si vous approuvez son contenu et faites confiance au site demandeur." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Demande de plafonnement des dépenses" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 3906abfbe181..cff9295abc33 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "इस संदेश को केवल तभी कन्फर्म करें जब आप कंटेंट को एप्रूव करते हैं और अनुरोध करने वाली साइट पर भरोसा करते हैं।" }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "खर्च करने की सीमा का अनुरोध" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 77fdbaa50ce3..8e2437d8c2bf 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Konfirmasikan pesan ini hanya jika Anda menyetujui isinya dan memercayai situs yang memintanya." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Permintaan batas penggunaan" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 1a762f1c713c..e1ca82a04a42 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "このメッセージの内容を承認し、要求元のサイトを信頼する場合にのみ確定してください。" }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "使用上限リクエスト" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f33c56f89bc5..2c36d6ad2f58 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "요청하는 사이트를 신뢰하고 그 내용을 완전히 이해하는 경우에만 이 메시지를 컨펌하세요." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "지출 한도 요청" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index c35e80ab3e41..27587daf31d0 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Confirme esta mensagem somente se você aprova o conteúdo e confia no site solicitante." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Solicitação de limite de gastos" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 94b3eda386b8..6b13eaf1a46f 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Подтверждайте это сообщение только в том случае, если вы одобряете его содержание и доверяете запрашивающему сайту." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Запрос лимита расходов" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index f859afba4482..624e9059634a 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Kumpirmahin lamang ang mensaheng ito kung ganap mong nauunawaan ang nilalaman at nagtitiwala sa site na humihiling." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Hiling sa limitasyon ng paggastos" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index bf7844af15f1..09ee23ea8d92 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Bu mesajı sadece içeriği onaylıyorsanız ve talepte bulunan siteye güveniyorsanız onaylayın." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Harcama üst limiti talebi" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 5c75f22b7e13..b1fa5514980e 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "Chỉ xác nhận thông báo này nếu bạn chấp thuận nội dung và tin tưởng trang web yêu cầu." }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "Yêu cầu hạn mức chi tiêu" }, "confirmTitleSIWESignature": { diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 55a4e0a70036..8733daf9431c 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1027,7 +1027,7 @@ "confirmTitleDescSignature": { "message": "仅在您批准该内容并信任请求网站的情况下,才能确认此消息。" }, - "confirmTitlePermitSignature": { + "confirmTitlePermitTokens": { "message": "支出上限请求" }, "confirmTitleSIWESignature": { diff --git a/app/scripts/background.js b/app/scripts/background.js index 45fcd0315c10..a563d0f81393 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -82,6 +82,8 @@ import { createOffscreen } from './offscreen'; /* eslint-enable import/first */ +import { COOKIE_ID_MARKETING_WHITELIST_ORIGINS } from './constants/marketing-site-whitelist'; + // eslint-disable-next-line @metamask/design-tokens/color-no-hex const BADGE_COLOR_APPROVAL = '#0376C9'; // eslint-disable-next-line @metamask/design-tokens/color-no-hex @@ -124,6 +126,7 @@ if (inTest || process.env.METAMASK_DEBUG) { } const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL); + // normalized (adds a trailing slash to the end of the domain if it's missing) // the URL once and reuse it: const phishingPageHref = phishingPageUrl.toString(); @@ -883,10 +886,10 @@ export function setupController( senderUrl.origin === phishingPageUrl.origin && senderUrl.pathname === phishingPageUrl.pathname ) { - const portStream = + const portStreamForPhishingPage = overrides?.getPortStream?.(remotePort) || new PortStream(remotePort); controller.setupPhishingCommunication({ - connectionStream: portStream, + connectionStream: portStreamForPhishingPage, }); } else { // this is triggered when a new tab is opened, or origin(url) is changed @@ -906,6 +909,18 @@ export function setupController( } }); } + if ( + senderUrl && + COOKIE_ID_MARKETING_WHITELIST_ORIGINS.some( + (origin) => origin === senderUrl.origin, + ) + ) { + const portStreamForCookieHandlerPage = + overrides?.getPortStream?.(remotePort) || new PortStream(remotePort); + controller.setUpCookieHandlerCommunication({ + connectionStream: portStreamForCookieHandlerPage, + }); + } connectExternalExtension(remotePort); } }; diff --git a/app/scripts/constants/marketing-site-whitelist.ts b/app/scripts/constants/marketing-site-whitelist.ts new file mode 100644 index 000000000000..8ff0cfd2de78 --- /dev/null +++ b/app/scripts/constants/marketing-site-whitelist.ts @@ -0,0 +1,15 @@ +export const COOKIE_ID_MARKETING_WHITELIST = [ + 'https://metamask.io', + 'https://learn.metamask.io', + 'https://mmi-support.zendesk.com', + 'https://community.metamask.io', + 'https://support.metamask.io', +]; + +if (process.env.IN_TEST) { + COOKIE_ID_MARKETING_WHITELIST.push('http://127.0.0.1:8080'); +} + +// Extract the origin of each URL in the whitelist +export const COOKIE_ID_MARKETING_WHITELIST_ORIGINS = + COOKIE_ID_MARKETING_WHITELIST.map((url) => new URL(url).origin); diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index df0238210d96..db78f948c31d 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -158,6 +158,7 @@ export const SENTRY_BACKGROUND_STATE = { segmentApiCalls: false, traits: false, dataCollectionForMarketing: false, + marketingCampaignCookieId: true, }, NameController: { names: false, diff --git a/app/scripts/constants/stream.ts b/app/scripts/constants/stream.ts new file mode 100644 index 000000000000..8552b49357dd --- /dev/null +++ b/app/scripts/constants/stream.ts @@ -0,0 +1,17 @@ +// contexts +export const CONTENT_SCRIPT = 'metamask-contentscript'; +export const METAMASK_INPAGE = 'metamask-inpage'; + +// stream channels +export const METAMASK_PROVIDER = 'metamask-provider'; +export const METAMASK_COOKIE_HANDLER = 'metamask-cookie-handler'; +export const PHISHING_SAFELIST = 'metamask-phishing-safelist'; +export const PHISHING_STREAM = 'phishing'; + +// For more information about these legacy streams, see here: +// https://github.com/MetaMask/metamask-extension/issues/15491 +// TODO:LegacyProvider: Delete +export const LEGACY_CONTENT_SCRIPT = 'contentscript'; +export const LEGACY_INPAGE = 'inpage'; +export const LEGACY_PROVIDER = 'provider'; +export const LEGACY_PUBLIC_CONFIG = 'publicConfig'; diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index ab2483c9f9a3..d0ff7119498b 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -9,6 +9,16 @@ import { getIsBrowserPrerenderBroken, } from '../../shared/modules/browser-runtime.utils'; import shouldInjectProvider from '../../shared/modules/provider-injection'; +import { + initializeCookieHandlerSteam, + isDetectedCookieMarketingSite, +} from './streams/cookie-handler-stream'; +import { logStreamDisconnectWarning } from './streams/shared'; +import { + METAMASK_COOKIE_HANDLER, + PHISHING_STREAM, + METAMASK_PROVIDER, +} from './constants/stream'; // contexts const CONTENT_SCRIPT = 'metamask-contentscript'; @@ -73,6 +83,11 @@ function setupPhishingPageStreams() { ); phishingPageChannel = phishingPageMux.createStream(PHISHING_SAFELIST); + phishingPageMux.ignoreStream(METAMASK_COOKIE_HANDLER); + phishingPageMux.ignoreStream(LEGACY_PUBLIC_CONFIG); + phishingPageMux.ignoreStream(LEGACY_PROVIDER); + phishingPageMux.ignoreStream(METAMASK_PROVIDER); + phishingPageMux.ignoreStream(PHISHING_STREAM); } const setupPhishingExtStreams = () => { @@ -116,6 +131,11 @@ const setupPhishingExtStreams = () => { error, ), ); + phishingExtMux.ignoreStream(METAMASK_COOKIE_HANDLER); + phishingExtMux.ignoreStream(LEGACY_PUBLIC_CONFIG); + phishingExtMux.ignoreStream(LEGACY_PROVIDER); + phishingExtMux.ignoreStream(METAMASK_PROVIDER); + phishingExtMux.ignoreStream(PHISHING_STREAM); // eslint-disable-next-line no-use-before-define phishingExtPort.onDisconnect.addListener(onDisconnectDestroyPhishingStreams); @@ -213,6 +233,11 @@ const setupPageStreams = () => { ); pageChannel = pageMux.createStream(PROVIDER); + pageMux.ignoreStream(METAMASK_COOKIE_HANDLER); + pageMux.ignoreStream(LEGACY_PROVIDER); + pageMux.ignoreStream(LEGACY_PUBLIC_CONFIG); + pageMux.ignoreStream(PHISHING_SAFELIST); + pageMux.ignoreStream(PHISHING_STREAM); }; // The field below is used to ensure that replay is done only once for each restart. @@ -248,6 +273,10 @@ const setupExtensionStreams = () => { extensionPhishingStream = extensionMux.createStream('phishing'); extensionPhishingStream.once('data', redirectToPhishingWarning); + extensionMux.ignoreStream(METAMASK_COOKIE_HANDLER); + extensionMux.ignoreStream(LEGACY_PROVIDER); + extensionMux.ignoreStream(PHISHING_SAFELIST); + // eslint-disable-next-line no-use-before-define extensionPort.onDisconnect.addListener(onDisconnectDestroyStreams); }; @@ -288,6 +317,11 @@ const setupLegacyPageStreams = () => { legacyPageMux.createStream(LEGACY_PROVIDER); legacyPagePublicConfigChannel = legacyPageMux.createStream(LEGACY_PUBLIC_CONFIG); + + legacyPageMux.ignoreStream(METAMASK_COOKIE_HANDLER); + legacyPageMux.ignoreStream(METAMASK_PROVIDER); + legacyPageMux.ignoreStream(PHISHING_SAFELIST); + legacyPageMux.ignoreStream(PHISHING_STREAM); }; // TODO:LegacyProvider: Delete @@ -331,6 +365,10 @@ const setupLegacyExtensionStreams = () => { error, ), ); + legacyExtMux.ignoreStream(METAMASK_COOKIE_HANDLER); + legacyExtMux.ignoreStream(LEGACY_PROVIDER); + legacyExtMux.ignoreStream(PHISHING_SAFELIST); + legacyExtMux.ignoreStream(PHISHING_STREAM); }; /** @@ -431,19 +469,6 @@ function getNotificationTransformStream() { return stream; } -/** - * Error handler for page to extension stream disconnections - * - * @param {string} remoteLabel - Remote stream name - * @param {Error} error - Stream connection error - */ -function logStreamDisconnectWarning(remoteLabel, error) { - console.debug( - `MetaMask: Content script lost connection to "${remoteLabel}".`, - error, - ); -} - /** * The function notifies inpage when the extension stream connection is ready. When the * 'metamask_chainChanged' method is received from the extension, it implies that the @@ -525,6 +550,10 @@ const start = () => { return; } + if (isDetectedCookieMarketingSite) { + initializeCookieHandlerSteam(); + } + if (shouldInjectProvider()) { initStreams(); diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 8aafa0893d53..5fa34ac0cbd1 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -155,6 +155,7 @@ export default class MetaMetricsController { participateInMetaMetrics: null, metaMetricsId: null, dataCollectionForMarketing: null, + marketingCampaignCookieId: null, eventsBeforeMetricsOptIn: [], traits: {}, previousUserTraits: {}, @@ -466,6 +467,8 @@ export default class MetaMetricsController { if (participateInMetaMetrics) { this.trackEventsAfterMetricsOptIn(); this.clearEventsAfterMetricsOptIn(); + } else if (this.state.marketingCampaignCookieId) { + this.setMarketingCampaignCookieId(null); } ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -477,10 +480,20 @@ export default class MetaMetricsController { setDataCollectionForMarketing(dataCollectionForMarketing) { const { metaMetricsId } = this.state; + this.store.updateState({ dataCollectionForMarketing }); + + if (!dataCollectionForMarketing && this.state.marketingCampaignCookieId) { + this.setMarketingCampaignCookieId(null); + } + return metaMetricsId; } + setMarketingCampaignCookieId(marketingCampaignCookieId) { + this.store.updateState({ marketingCampaignCookieId }); + } + get state() { return this.store.getState(); } @@ -704,6 +717,7 @@ export default class MetaMetricsController { userAgent: window.navigator.userAgent, page, referrer, + marketingCampaignCookieId: this.state.marketingCampaignCookieId, }; } diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 40a3f2795f01..de75170b8e58 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -18,6 +18,7 @@ const VERSION = '0.0.1-test'; const FAKE_CHAIN_ID = '0x1338'; const LOCALE = 'en_US'; const TEST_META_METRICS_ID = '0xabc'; +const TEST_GA_COOKIE_ID = '123456.123455'; const DUMMY_ACTION_ID = 'DUMMY_ACTION_ID'; const MOCK_EXTENSION_ID = 'testid'; @@ -51,6 +52,7 @@ const DEFAULT_TEST_CONTEXT = { page: METAMETRICS_BACKGROUND_PAGE_OBJECT, referrer: undefined, userAgent: window.navigator.userAgent, + marketingCampaignCookieId: null, }; const DEFAULT_SHARED_PROPERTIES = { @@ -114,6 +116,7 @@ const SAMPLE_NON_PERSISTED_EVENT = { function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, + marketingCampaignCookieId = null, preferencesStore = getMockPreferencesStore(), getCurrentChainId = () => FAKE_CHAIN_ID, onNetworkDidChange = () => { @@ -131,6 +134,7 @@ function getMetaMetricsController({ initState: { participateInMetaMetrics, metaMetricsId, + marketingCampaignCookieId, fragments: { testid: SAMPLE_PERSISTED_EVENT, testid2: SAMPLE_NON_PERSISTED_EVENT, @@ -161,6 +165,9 @@ describe('MetaMetricsController', function () { expect(metaMetricsController.state.metaMetricsId).toStrictEqual( TEST_META_METRICS_ID, ); + expect( + metaMetricsController.state.marketingCampaignCookieId, + ).toStrictEqual(null); expect(metaMetricsController.locale).toStrictEqual( LOCALE.replace('_', '-'), ); @@ -340,6 +347,21 @@ describe('MetaMetricsController', function () { TEST_META_METRICS_ID, ); }); + it('should nullify the marketingCampaignCookieId when participateInMetaMetrics is toggled off', async function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + metaMetricsId: TEST_META_METRICS_ID, + dataCollectionForMarketing: true, + marketingCampaignCookieId: TEST_GA_COOKIE_ID, + }); + expect( + metaMetricsController.state.marketingCampaignCookieId, + ).toStrictEqual(TEST_GA_COOKIE_ID); + await metaMetricsController.setParticipateInMetaMetrics(false); + expect( + metaMetricsController.state.marketingCampaignCookieId, + ).toStrictEqual(null); + }); }); describe('submitEvent', function () { @@ -1243,7 +1265,65 @@ describe('MetaMetricsController', function () { expect(Object.keys(segmentApiCalls).length === 0).toStrictEqual(true); }); }); - + describe('setMarketingCampaignCookieId', function () { + it('should update marketingCampaignCookieId in the context when cookieId is available', async function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + metaMetricsId: TEST_META_METRICS_ID, + dataCollectionForMarketing: true, + }); + metaMetricsController.setMarketingCampaignCookieId(TEST_GA_COOKIE_ID); + expect( + metaMetricsController.state.marketingCampaignCookieId, + ).toStrictEqual(TEST_GA_COOKIE_ID); + const spy = jest.spyOn(segment, 'track'); + metaMetricsController.submitEvent( + { + event: 'Fake Event', + category: 'Unit Test', + properties: { + test: 1, + }, + }, + { isOptIn: true }, + ); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + { + event: 'Fake Event', + anonymousId: METAMETRICS_ANONYMOUS_ID, + context: { + ...DEFAULT_TEST_CONTEXT, + marketingCampaignCookieId: TEST_GA_COOKIE_ID, + }, + properties: { + test: 1, + ...DEFAULT_EVENT_PROPERTIES, + }, + messageId: Utils.generateRandomId(), + timestamp: new Date(), + }, + spy.mock.calls[0][1], + ); + }); + }); + describe('setDataCollectionForMarketing', function () { + it('should nullify the marketingCampaignCookieId when Data collection for marketing is toggled off', async function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + metaMetricsId: TEST_META_METRICS_ID, + dataCollectionForMarketing: true, + marketingCampaignCookieId: TEST_GA_COOKIE_ID, + }); + expect( + metaMetricsController.state.marketingCampaignCookieId, + ).toStrictEqual(TEST_GA_COOKIE_ID); + await metaMetricsController.setDataCollectionForMarketing(false); + expect( + metaMetricsController.state.marketingCampaignCookieId, + ).toStrictEqual(null); + }); + }); afterEach(function () { // flush the queues manually after each test segment.flush(); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 1ba853a1b846..7cc024d0cd47 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -41,7 +41,7 @@ import AccountTracker from '../lib/account-tracker'; import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; -import { PreferencesController } from './preferences'; +import PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; type UpdateCustodianTransactionsParameters = { diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences-controller.test.ts similarity index 82% rename from app/scripts/controllers/preferences.test.js rename to app/scripts/controllers/preferences-controller.test.ts index 60784db8984f..d35a35b24f51 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -2,11 +2,25 @@ * @jest-environment node */ import { ControllerMessenger } from '@metamask/base-controller'; -import { TokenListController } from '@metamask/assets-controllers'; import { AccountsController } from '@metamask/accounts-controller'; +import { + KeyringControllerGetAccountsAction, + KeyringControllerGetKeyringsByTypeAction, + KeyringControllerGetKeyringForAccountAction, + KeyringControllerStateChangeEvent, + KeyringControllerAccountRemovedEvent, +} from '@metamask/keyring-controller'; +import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { mockNetworkState } from '../../../test/stub/networks'; -import PreferencesController from './preferences'; +import { ThemeType } from '../../../shared/constants/preferences'; +import type { + AllowedActions, + AllowedEvents, + PreferencesControllerActions, + PreferencesControllerEvents, +} from './preferences-controller'; +import PreferencesController from './preferences-controller'; const NETWORK_CONFIGURATION_DATA = mockNetworkState( { @@ -14,7 +28,6 @@ const NETWORK_CONFIGURATION_DATA = mockNetworkState( rpcUrl: 'https://testrpc.com', chainId: CHAIN_IDS.GOERLI, nickname: '0X5', - rpcPrefs: { blockExplorerUrl: 'https://etherscan.io' }, }, { id: 'test-networkConfigurationId-2', @@ -22,15 +35,24 @@ const NETWORK_CONFIGURATION_DATA = mockNetworkState( chainId: '0x539', ticker: 'ETH', nickname: 'Localhost 8545', - rpcPrefs: {}, }, ).networkConfigurations; describe('preferences controller', () => { - let controllerMessenger; - let preferencesController; - let accountsController; - let tokenListController; + let controllerMessenger: ControllerMessenger< + | PreferencesControllerActions + | AllowedActions + | KeyringControllerGetAccountsAction + | KeyringControllerGetKeyringsByTypeAction + | KeyringControllerGetKeyringForAccountAction, + | PreferencesControllerEvents + | KeyringControllerStateChangeEvent + | KeyringControllerAccountRemovedEvent + | SnapControllerStateChangeEvent + | AllowedEvents + >; + let preferencesController: PreferencesController; + let accountsController: AccountsController; beforeEach(() => { controllerMessenger = new ControllerMessenger(); @@ -49,19 +71,15 @@ describe('preferences controller', () => { ], }); + const mockAccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; accountsController = new AccountsController({ messenger: accountsControllerMessenger, - }); - - const tokenListMessenger = controllerMessenger.getRestricted({ - name: 'TokenListController', - }); - tokenListController = new TokenListController({ - chainId: '1', - preventPollingOnNetworkRestart: false, - onNetworkStateChange: jest.fn(), - onPreferencesStateChange: jest.fn(), - messenger: tokenListMessenger, + state: mockAccountsControllerState, }); const preferencesMessenger = controllerMessenger.getRestricted({ @@ -76,7 +94,6 @@ describe('preferences controller', () => { preferencesController = new PreferencesController({ initLangCode: 'en_US', - tokenListController, networkConfigurations: NETWORK_CONFIGURATION_DATA, messenger: preferencesMessenger, }); @@ -116,20 +133,22 @@ describe('preferences controller', () => { const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; it('updating name from preference controller will update the name in accounts controller and preferences controller', () => { - controllerMessenger.publish('KeyringController:stateChange', { - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [firstAddress, secondAddress], - }, - ], - }); + controllerMessenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [firstAddress, secondAddress], + }, + ], + }, + [], + ); let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); - const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; @@ -160,15 +179,19 @@ describe('preferences controller', () => { }); it('updating name from accounts controller updates the name in preferences controller', () => { - controllerMessenger.publish('KeyringController:stateChange', { - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [firstAddress, secondAddress], - }, - ], - }); + controllerMessenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [firstAddress, secondAddress], + }, + ], + }, + [], + ); let [firstAccount, secondAccount] = accountsController.listAccounts(); @@ -207,15 +230,19 @@ describe('preferences controller', () => { it('updating selectedAddress from preferences controller updates the selectedAccount in accounts controller and preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish('KeyringController:stateChange', { - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [firstAddress, secondAddress], - }, - ], - }); + controllerMessenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [firstAddress, secondAddress], + }, + ], + }, + [], + ); const selectedAccount = accountsController.getSelectedAccount(); @@ -237,15 +264,19 @@ describe('preferences controller', () => { it('updating selectedAccount from accounts controller updates the selectedAddress in preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish('KeyringController:stateChange', { - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [firstAddress, secondAddress], - }, - ], - }); + controllerMessenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [firstAddress, secondAddress], + }, + ], + }, + [], + ); const selectedAccount = accountsController.getSelectedAccount(); const accounts = accountsController.listAccounts(); @@ -355,9 +386,20 @@ describe('preferences controller', () => { }); it('should keep initial value of useTokenDetection for existing users', function () { + // TODO: Remove unregisterActionHandler and clearEventSubscriptions once the PreferencesController has been refactored to use the withController pattern. + controllerMessenger.unregisterActionHandler( + 'PreferencesController:getState', + ); + controllerMessenger.clearEventSubscriptions( + 'PreferencesController:stateChange', + ); const preferencesControllerExistingUser = new PreferencesController({ + messenger: controllerMessenger.getRestricted({ + name: 'PreferencesController', + allowedActions: [], + allowedEvents: ['AccountsController:stateChange'], + }), initLangCode: 'en_US', - tokenListController, initState: { useTokenDetection: false, }, @@ -446,7 +488,7 @@ describe('preferences controller', () => { }); it('should set the setTheme property in state', () => { - preferencesController.setTheme('dark'); + preferencesController.setTheme(ThemeType.dark); expect(preferencesController.store.getState().theme).toStrictEqual( 'dark', ); @@ -472,7 +514,9 @@ describe('preferences controller', () => { const addedNonTestNetworks = Object.keys(NETWORK_CONFIGURATION_DATA); it('should have default value combined', () => { - const state = preferencesController.store.getState(); + const state: { + incomingTransactionsPreferences: Record; + } = preferencesController.store.getState(); expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, @@ -486,10 +530,12 @@ describe('preferences controller', () => { it('should update incomingTransactionsPreferences with given value set', () => { preferencesController.setIncomingTransactionsPreferences( - [CHAIN_IDS.LINEA_MAINNET], + CHAIN_IDS.LINEA_MAINNET, false, ); - const state = preferencesController.store.getState(); + const state: { + incomingTransactionsPreferences: Record; + } = preferencesController.store.getState(); expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: false, @@ -506,15 +552,19 @@ describe('preferences controller', () => { it('sync the identities with the accounts in the accounts controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish('KeyringController:stateChange', { - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [firstAddress, secondAddress], - }, - ], - }); + controllerMessenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [firstAddress, secondAddress], + }, + ], + }, + [], + ); const accounts = accountsController.listAccounts(); diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences-controller.ts similarity index 52% rename from app/scripts/controllers/preferences.js rename to app/scripts/controllers/preferences-controller.ts index 677761cd2ef5..8a3451b8b163 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences-controller.ts @@ -1,4 +1,14 @@ import { ObservableStore } from '@metamask/obs-store'; +import { + AccountsControllerChangeEvent, + AccountsControllerGetAccountByAddressAction, + AccountsControllerGetSelectedAccountAction, + AccountsControllerSetAccountNameAction, + AccountsControllerSetSelectedAccountAction, + AccountsControllerState, +} from '@metamask/accounts-controller'; +import { Hex } from '@metamask/utils'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; import { CHAIN_IDS, IPFS_DEFAULT_GATEWAY_URL, @@ -6,6 +16,12 @@ import { import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets'; import { ThemeType } from '../../../shared/constants/preferences'; +type AccountIdentityEntry = { + address: string; + name: string; + lastSelected: number | undefined; +}; + const mainNetworks = { [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, @@ -17,32 +33,151 @@ const testNetworks = { [CHAIN_IDS.LINEA_SEPOLIA]: true, }; +const controllerName = 'PreferencesController'; + +/** + * Returns the state of the {@link PreferencesController}. + */ +export type PreferencesControllerGetStateAction = { + type: 'PreferencesController:getState'; + handler: () => PreferencesControllerState; +}; + +/** + * Actions exposed by the {@link PreferencesController}. + */ +export type PreferencesControllerActions = PreferencesControllerGetStateAction; + +/** + * Event emitted when the state of the {@link PreferencesController} changes. + */ +export type PreferencesControllerStateChangeEvent = { + type: 'PreferencesController:stateChange'; + payload: [PreferencesControllerState, []]; +}; + +/** + * Events emitted by {@link PreferencesController}. + */ +export type PreferencesControllerEvents = PreferencesControllerStateChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | AccountsControllerGetAccountByAddressAction + | AccountsControllerSetAccountNameAction + | AccountsControllerGetSelectedAccountAction + | AccountsControllerSetSelectedAccountAction; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerChangeEvent; + +export type PreferencesControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + PreferencesControllerActions | AllowedActions, + PreferencesControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +type PreferencesControllerOptions = { + networkConfigurations?: Record; + initState?: Partial; + initLangCode?: string; + messenger: PreferencesControllerMessenger; +}; + +export type Preferences = { + autoLockTimeLimit?: number; + showExtensionInFullSizeView: boolean; + showFiatInTestnets: boolean; + showTestNetworks: boolean; + smartTransactionsOptInStatus: boolean | null; + useNativeCurrencyAsPrimaryCurrency: boolean; + hideZeroBalanceTokens: boolean; + petnamesEnabled: boolean; + redesignedConfirmationsEnabled: boolean; + redesignedTransactionsEnabled: boolean; + featureNotificationsEnabled: boolean; + isRedesignedConfirmationsDeveloperEnabled: boolean; + showConfirmationAdvancedDetails: boolean; +}; + +export type PreferencesControllerState = { + selectedAddress: string; + useBlockie: boolean; + useNonceField: boolean; + usePhishDetect: boolean; + dismissSeedBackUpReminder: boolean; + useMultiAccountBalanceChecker: boolean; + useSafeChainsListValidation: boolean; + useTokenDetection: boolean; + useNftDetection: boolean; + use4ByteResolution: boolean; + useCurrencyRateCheck: boolean; + useRequestQueue: boolean; + openSeaEnabled: boolean; + securityAlertsEnabled: boolean; + watchEthereumAccountEnabled: boolean; + bitcoinSupportEnabled: boolean; + bitcoinTestnetSupportEnabled: boolean; + addSnapAccountEnabled: boolean; + advancedGasFee: Record>; + featureFlags: Record; + incomingTransactionsPreferences: Record; + knownMethodData: Record; + currentLocale: string; + identities: Record; + lostIdentities: Record; + forgottenPassword: boolean; + preferences: Preferences; + ipfsGateway: string; + isIpfsGatewayEnabled: boolean; + useAddressBarEnsResolution: boolean; + ledgerTransportType: LedgerTransportTypes; + snapRegistryList: Record; + theme: ThemeType; + snapsAddSnapAccountModalDismissed: boolean; + useExternalNameSources: boolean; + useTransactionSimulations: boolean; + enableMV3TimestampSave: boolean; + useExternalServices: boolean; + textDirection?: string; +}; + export default class PreferencesController { + store: ObservableStore; + + private messagingSystem: PreferencesControllerMessenger; + /** * - * @typedef {object} PreferencesController - * @param {object} opts - Overrides the defaults for the initial state of this.store - * @property {object} messenger - The controller messenger - * @property {object} store The stored object containing a users preferences, stored in local storage - * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI - * @property {boolean} store.useNonceField The users preference for nonce field within the UI - * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the + * @param opts - Overrides the defaults for the initial state of this.store + * @property messenger - The controller messenger + * @property initState The stored object containing a users preferences, stored in local storage + * @property initState.useBlockie The users preference for blockie identicons within the UI + * @property initState.useNonceField The users preference for nonce field within the UI + * @property initState.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature. * * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior. - * @property {object} store.knownMethodData Contains all data methods known by the user - * @property {string} store.currentLocale The preferred language locale key - * @property {string} store.selectedAddress A hex string that matches the currently selected address in the app - */ - constructor(opts = {}) { - const addedNonMainNetwork = Object.values( - opts.networkConfigurations, - ).reduce((acc, element) => { + * @property initState.knownMethodData Contains all data methods known by the user + * @property initState.currentLocale The preferred language locale key + * @property initState.selectedAddress A hex string that matches the currently selected address in the app + */ + constructor(opts: PreferencesControllerOptions) { + const addedNonMainNetwork: Record = Object.values( + opts.networkConfigurations ?? {}, + ).reduce((acc: Record, element) => { acc[element.chainId] = true; return acc; }, {}); - const initState = { + const initState: PreferencesControllerState = { + selectedAddress: '', useBlockie: false, useNonceField: false, usePhishDetect: true, @@ -56,7 +191,7 @@ export default class PreferencesController { use4ByteResolution: true, useCurrencyRateCheck: true, useRequestQueue: true, - openSeaEnabled: true, // todo set this to true + openSeaEnabled: true, securityAlertsEnabled: true, watchEthereumAccountEnabled: false, bitcoinSupportEnabled: false, @@ -77,7 +212,7 @@ export default class PreferencesController { ...testNetworks, }, knownMethodData: {}, - currentLocale: opts.initLangCode, + currentLocale: opts.initLangCode ?? '', identities: {}, lostIdentities: {}, forgottenPassword: false, @@ -120,87 +255,84 @@ export default class PreferencesController { ...opts.initState, }; - this.network = opts.network; - this.store = new ObservableStore(initState); this.store.setMaxListeners(13); this.messagingSystem = opts.messenger; - this.messagingSystem?.registerActionHandler( + this.messagingSystem.registerActionHandler( `PreferencesController:getState`, () => this.store.getState(), ); - this.messagingSystem?.registerInitialEventPayload({ + this.messagingSystem.registerInitialEventPayload({ eventType: `PreferencesController:stateChange`, getPayload: () => [this.store.getState(), []], }); - this.messagingSystem?.subscribe( + this.messagingSystem.subscribe( 'AccountsController:stateChange', this.#handleAccountsControllerSync.bind(this), ); - global.setPreference = (key, value) => { + globalThis.setPreference = (key: keyof Preferences, value: boolean) => { return this.setFeatureFlag(key, value); }; } - // PUBLIC METHODS /** * Sets the {@code forgottenPassword} state property * - * @param {boolean} forgottenPassword - whether or not the user has forgotten their password + * @param forgottenPassword - whether or not the user has forgotten their password */ - setPasswordForgotten(forgottenPassword) { + setPasswordForgotten(forgottenPassword: boolean): void { this.store.updateState({ forgottenPassword }); } /** * Setter for the `useBlockie` property * - * @param {boolean} val - Whether or not the user prefers blockie indicators + * @param val - Whether or not the user prefers blockie indicators */ - setUseBlockie(val) { + setUseBlockie(val: boolean): void { this.store.updateState({ useBlockie: val }); } /** * Setter for the `useNonceField` property * - * @param {boolean} val - Whether or not the user prefers to set nonce + * @param val - Whether or not the user prefers to set nonce */ - setUseNonceField(val) { + setUseNonceField(val: boolean): void { this.store.updateState({ useNonceField: val }); } /** * Setter for the `usePhishDetect` property * - * @param {boolean} val - Whether or not the user prefers phishing domain protection + * @param val - Whether or not the user prefers phishing domain protection */ - setUsePhishDetect(val) { + setUsePhishDetect(val: boolean): void { this.store.updateState({ usePhishDetect: val }); } /** * Setter for the `useMultiAccountBalanceChecker` property * - * @param {boolean} val - Whether or not the user prefers to turn off/on all security settings + * @param val - Whether or not the user prefers to turn off/on all security settings */ - setUseMultiAccountBalanceChecker(val) { + setUseMultiAccountBalanceChecker(val: boolean): void { this.store.updateState({ useMultiAccountBalanceChecker: val }); } /** * Setter for the `useSafeChainsListValidation` property * - * @param {boolean} val - Whether or not the user prefers to turn off/on validation for manually adding networks + * @param val - Whether or not the user prefers to turn off/on validation for manually adding networks */ - setUseSafeChainsListValidation(val) { + setUseSafeChainsListValidation(val: boolean): void { this.store.updateState({ useSafeChainsListValidation: val }); } - toggleExternalServices(useExternalServices) { + toggleExternalServices(useExternalServices: boolean): void { this.store.updateState({ useExternalServices }); this.setUseTokenDetection(useExternalServices); this.setUseCurrencyRateCheck(useExternalServices); @@ -213,54 +345,54 @@ export default class PreferencesController { /** * Setter for the `useTokenDetection` property * - * @param {boolean} val - Whether or not the user prefers to use the static token list or dynamic token list from the API + * @param val - Whether or not the user prefers to use the static token list or dynamic token list from the API */ - setUseTokenDetection(val) { + setUseTokenDetection(val: boolean): void { this.store.updateState({ useTokenDetection: val }); } /** * Setter for the `useNftDetection` property * - * @param {boolean} useNftDetection - Whether or not the user prefers to autodetect NFTs. + * @param useNftDetection - Whether or not the user prefers to autodetect NFTs. */ - setUseNftDetection(useNftDetection) { + setUseNftDetection(useNftDetection: boolean): void { this.store.updateState({ useNftDetection }); } /** * Setter for the `use4ByteResolution` property * - * @param {boolean} use4ByteResolution - (Privacy) Whether or not the user prefers to have smart contract name details resolved with 4byte.directory + * @param use4ByteResolution - (Privacy) Whether or not the user prefers to have smart contract name details resolved with 4byte.directory */ - setUse4ByteResolution(use4ByteResolution) { + setUse4ByteResolution(use4ByteResolution: boolean): void { this.store.updateState({ use4ByteResolution }); } /** * Setter for the `useCurrencyRateCheck` property * - * @param {boolean} val - Whether or not the user prefers to use currency rate check for ETH and tokens. + * @param val - Whether or not the user prefers to use currency rate check for ETH and tokens. */ - setUseCurrencyRateCheck(val) { + setUseCurrencyRateCheck(val: boolean): void { this.store.updateState({ useCurrencyRateCheck: val }); } /** * Setter for the `useRequestQueue` property * - * @param {boolean} val - Whether or not the user wants to have requests queued if network change is required. + * @param val - Whether or not the user wants to have requests queued if network change is required. */ - setUseRequestQueue(val) { + setUseRequestQueue(val: boolean): void { this.store.updateState({ useRequestQueue: val }); } /** * Setter for the `openSeaEnabled` property * - * @param {boolean} openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data. + * @param openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data. */ - setOpenSeaEnabled(openSeaEnabled) { + setOpenSeaEnabled(openSeaEnabled: boolean): void { this.store.updateState({ openSeaEnabled, }); @@ -269,9 +401,9 @@ export default class PreferencesController { /** * Setter for the `securityAlertsEnabled` property * - * @param {boolean} securityAlertsEnabled - Whether or not the user prefers to use the security alerts. + * @param securityAlertsEnabled - Whether or not the user prefers to use the security alerts. */ - setSecurityAlertsEnabled(securityAlertsEnabled) { + setSecurityAlertsEnabled(securityAlertsEnabled: boolean): void { this.store.updateState({ securityAlertsEnabled, }); @@ -281,10 +413,10 @@ export default class PreferencesController { /** * Setter for the `addSnapAccountEnabled` property. * - * @param {boolean} addSnapAccountEnabled - Whether or not the user wants to + * @param addSnapAccountEnabled - Whether or not the user wants to * enable the "Add Snap accounts" button. */ - setAddSnapAccountEnabled(addSnapAccountEnabled) { + setAddSnapAccountEnabled(addSnapAccountEnabled: boolean): void { this.store.updateState({ addSnapAccountEnabled, }); @@ -294,10 +426,10 @@ export default class PreferencesController { /** * Setter for the `watchEthereumAccountEnabled` property. * - * @param {boolean} watchEthereumAccountEnabled - Whether or not the user wants to + * @param watchEthereumAccountEnabled - Whether or not the user wants to * enable the "Watch Ethereum account (Beta)" button. */ - setWatchEthereumAccountEnabled(watchEthereumAccountEnabled) { + setWatchEthereumAccountEnabled(watchEthereumAccountEnabled: boolean): void { this.store.updateState({ watchEthereumAccountEnabled, }); @@ -306,10 +438,10 @@ export default class PreferencesController { /** * Setter for the `bitcoinSupportEnabled` property. * - * @param {boolean} bitcoinSupportEnabled - Whether or not the user wants to + * @param bitcoinSupportEnabled - Whether or not the user wants to * enable the "Add a new Bitcoin account (Beta)" button. */ - setBitcoinSupportEnabled(bitcoinSupportEnabled) { + setBitcoinSupportEnabled(bitcoinSupportEnabled: boolean): void { this.store.updateState({ bitcoinSupportEnabled, }); @@ -318,10 +450,10 @@ export default class PreferencesController { /** * Setter for the `bitcoinTestnetSupportEnabled` property. * - * @param {boolean} bitcoinTestnetSupportEnabled - Whether or not the user wants to + * @param bitcoinTestnetSupportEnabled - Whether or not the user wants to * enable the "Add a new Bitcoin account (Testnet)" button. */ - setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled) { + setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled: boolean): void { this.store.updateState({ bitcoinTestnetSupportEnabled, }); @@ -330,9 +462,9 @@ export default class PreferencesController { /** * Setter for the `useExternalNameSources` property * - * @param {boolean} useExternalNameSources - Whether or not to use external name providers in the name controller. + * @param useExternalNameSources - Whether or not to use external name providers in the name controller. */ - setUseExternalNameSources(useExternalNameSources) { + setUseExternalNameSources(useExternalNameSources: boolean): void { this.store.updateState({ useExternalNameSources, }); @@ -341,9 +473,9 @@ export default class PreferencesController { /** * Setter for the `useTransactionSimulations` property * - * @param {boolean} useTransactionSimulations - Whether or not to use simulations in the transaction confirmations. + * @param useTransactionSimulations - Whether or not to use simulations in the transaction confirmations. */ - setUseTransactionSimulations(useTransactionSimulations) { + setUseTransactionSimulations(useTransactionSimulations: boolean): void { this.store.updateState({ useTransactionSimulations, }); @@ -352,11 +484,17 @@ export default class PreferencesController { /** * Setter for the `advancedGasFee` property * - * @param {object} options - * @param {string} options.chainId - The chainId the advancedGasFees should be set on - * @param {object} options.gasFeePreferences - The advancedGasFee options to set - */ - setAdvancedGasFee({ chainId, gasFeePreferences }) { + * @param options + * @param options.chainId - The chainId the advancedGasFees should be set on + * @param options.gasFeePreferences - The advancedGasFee options to set + */ + setAdvancedGasFee({ + chainId, + gasFeePreferences, + }: { + chainId: string; + gasFeePreferences: Record; + }): void { const { advancedGasFee } = this.store.getState(); this.store.updateState({ advancedGasFee: { @@ -369,19 +507,19 @@ export default class PreferencesController { /** * Setter for the `theme` property * - * @param {string} val - 'default' or 'dark' value based on the mode selected by user. + * @param val - 'default' or 'dark' value based on the mode selected by user. */ - setTheme(val) { + setTheme(val: ThemeType): void { this.store.updateState({ theme: val }); } /** * Add new methodData to state, to avoid requesting this information again through Infura * - * @param {string} fourBytePrefix - Four-byte method signature - * @param {string} methodData - Corresponding data method + * @param fourBytePrefix - Four-byte method signature + * @param methodData - Corresponding data method */ - addKnownMethodData(fourBytePrefix, methodData) { + addKnownMethodData(fourBytePrefix: string, methodData: string): void { const { knownMethodData } = this.store.getState(); knownMethodData[fourBytePrefix] = methodData; this.store.updateState({ knownMethodData }); @@ -390,9 +528,9 @@ export default class PreferencesController { /** * Setter for the `currentLocale` property * - * @param {string} key - he preferred language locale key + * @param key - he preferred language locale key */ - setCurrentLocale(key) { + setCurrentLocale(key: string): string { const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key) ? 'rtl' : 'auto'; @@ -407,9 +545,9 @@ export default class PreferencesController { * Setter for the `selectedAddress` property * * @deprecated - Use setSelectedAccount from the AccountsController - * @param {string} address - A new hex address for an account + * @param address - A new hex address for an account */ - setSelectedAddress(address) { + setSelectedAddress(address: string): void { const account = this.messagingSystem.call( 'AccountsController:getAccountByAddress', address, @@ -428,9 +566,9 @@ export default class PreferencesController { * Getter for the `selectedAddress` property * * @deprecated - Use the getSelectedAccount from the AccountsController - * @returns {string} The hex address for the currently selected account + * @returns The hex address for the currently selected account */ - getSelectedAddress() { + getSelectedAddress(): string { const selectedAccount = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); @@ -441,9 +579,9 @@ export default class PreferencesController { /** * Getter for the `useRequestQueue` property * - * @returns {boolean} whether this option is on or off. + * @returns whether this option is on or off. */ - getUseRequestQueue() { + getUseRequestQueue(): boolean { return this.store.getState().useRequestQueue; } @@ -451,38 +589,42 @@ export default class PreferencesController { * Sets a custom label for an account * * @deprecated - Use setAccountName from the AccountsController - * @param {string} address - the account to set a label for - * @param {string} label - the custom label for the account - * @returns {Promise} + * @param address - the account to set a label for + * @param label - the custom label for the account + * @returns the account label */ - async setAccountLabel(address, label) { - const account = this.messagingSystem.call( - 'AccountsController:getAccountByAddress', - address, - ); + setAccountLabel(address: string, label: string): string | undefined { if (!address) { throw new Error( `setAccountLabel requires a valid address, got ${String(address)}`, ); } - this.messagingSystem.call( - 'AccountsController:setAccountName', - account.id, - label, + const account = this.messagingSystem.call( + 'AccountsController:getAccountByAddress', + address, ); + if (account) { + this.messagingSystem.call( + 'AccountsController:setAccountName', + account.id, + label, + ); - return label; + return label; + } + + return undefined; } /** * Updates the `featureFlags` property, which is an object. One property within that object will be set to a boolean. * - * @param {string} feature - A key that corresponds to a UI feature. - * @param {boolean} activated - Indicates whether or not the UI feature should be displayed - * @returns {Promise} Promises a new object; the updated featureFlags object. + * @param feature - A key that corresponds to a UI feature. + * @param activated - Indicates whether or not the UI feature should be displayed + * @returns the updated featureFlags object. */ - async setFeatureFlag(feature, activated) { + setFeatureFlag(feature: string, activated: boolean): Record { const currentFeatureFlags = this.store.getState().featureFlags; const updatedFeatureFlags = { ...currentFeatureFlags, @@ -498,11 +640,14 @@ export default class PreferencesController { * Updates the `preferences` property, which is an object. These are user-controlled features * found in the settings page. * - * @param {string} preference - The preference to enable or disable. - * @param {boolean |object} value - Indicates whether or not the preference should be enabled or disabled. - * @returns {Promise} Promises a new object; the updated preferences object. + * @param preference - The preference to enable or disable. + * @param value - Indicates whether or not the preference should be enabled or disabled. + * @returns Promises a updated Preferences object. */ - async setPreference(preference, value) { + setPreference( + preference: keyof Preferences, + value: Preferences[typeof preference], + ): Preferences { const currentPreferences = this.getPreferences(); const updatedPreferences = { ...currentPreferences, @@ -516,28 +661,28 @@ export default class PreferencesController { /** * A getter for the `preferences` property * - * @returns {object} A key-boolean map of user-selected preferences. + * @returns A map of user-selected preferences. */ - getPreferences() { + getPreferences(): Preferences { return this.store.getState().preferences; } /** * A getter for the `ipfsGateway` property * - * @returns {string} The current IPFS gateway domain + * @returns The current IPFS gateway domain */ - getIpfsGateway() { + getIpfsGateway(): string { return this.store.getState().ipfsGateway; } /** * A setter for the `ipfsGateway` property * - * @param {string} domain - The new IPFS gateway domain - * @returns {Promise} A promise of the update IPFS gateway domain + * @param domain - The new IPFS gateway domain + * @returns the update IPFS gateway domain */ - async setIpfsGateway(domain) { + setIpfsGateway(domain: string): string { this.store.updateState({ ipfsGateway: domain }); return domain; } @@ -545,18 +690,18 @@ export default class PreferencesController { /** * A setter for the `isIpfsGatewayEnabled` property * - * @param {boolean} enabled - Whether or not IPFS is enabled + * @param enabled - Whether or not IPFS is enabled */ - async setIsIpfsGatewayEnabled(enabled) { + setIsIpfsGatewayEnabled(enabled: boolean): void { this.store.updateState({ isIpfsGatewayEnabled: enabled }); } /** * A setter for the `useAddressBarEnsResolution` property * - * @param {boolean} useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains + * @param useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains */ - async setUseAddressBarEnsResolution(useAddressBarEnsResolution) { + setUseAddressBarEnsResolution(useAddressBarEnsResolution: boolean): void { this.store.updateState({ useAddressBarEnsResolution }); } @@ -565,10 +710,12 @@ export default class PreferencesController { * * @deprecated We no longer support specifying a ledger transport type other * than webhid, therefore managing a preference is no longer necessary. - * @param {LedgerTransportTypes.webhid} ledgerTransportType - 'webhid' - * @returns {string} The transport type that was set. + * @param ledgerTransportType - 'webhid' + * @returns The transport type that was set. */ - setLedgerTransportPreference(ledgerTransportType) { + setLedgerTransportPreference( + ledgerTransportType: LedgerTransportTypes, + ): string { this.store.updateState({ ledgerTransportType }); return ledgerTransportType; } @@ -576,10 +723,10 @@ export default class PreferencesController { /** * A setter for the user preference to dismiss the seed phrase backup reminder * - * @param {bool} dismissSeedBackUpReminder - User preference for dismissing the back up reminder. + * @param dismissSeedBackUpReminder - User preference for dismissing the back up reminder. */ - async setDismissSeedBackUpReminder(dismissSeedBackUpReminder) { - await this.store.updateState({ + setDismissSeedBackUpReminder(dismissSeedBackUpReminder: boolean): void { + this.store.updateState({ dismissSeedBackUpReminder, }); } @@ -587,29 +734,30 @@ export default class PreferencesController { /** * A setter for the incomingTransactions in preference to be updated * - * @param {string} chainId - chainId of the network - * @param {bool} value - preference of certain network, true to be enabled + * @param chainId - chainId of the network + * @param value - preference of certain network, true to be enabled */ - setIncomingTransactionsPreferences(chainId, value) { + setIncomingTransactionsPreferences(chainId: Hex, value: boolean): void { const previousValue = this.store.getState().incomingTransactionsPreferences; const updatedValue = { ...previousValue, [chainId]: value }; this.store.updateState({ incomingTransactionsPreferences: updatedValue }); } - setServiceWorkerKeepAlivePreference(value) { + setServiceWorkerKeepAlivePreference(value: boolean): void { this.store.updateState({ enableMV3TimestampSave: value }); } ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - setSnapsAddSnapAccountModalDismissed(value) { + setSnapsAddSnapAccountModalDismissed(value: boolean): void { this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); } ///: END:ONLY_INCLUDE_IF - #handleAccountsControllerSync(newAccountsControllerState) { + #handleAccountsControllerSync( + newAccountsControllerState: AccountsControllerState, + ): void { const { accounts, selectedAccount: selectedAccountId } = newAccountsControllerState.internalAccounts; - const selectedAccount = accounts[selectedAccountId]; const { identities, lostIdentities } = this.store.getState(); @@ -624,7 +772,7 @@ export default class PreferencesController { }); const updatedIdentities = Object.values(accounts).reduce( - (identitiesMap, account) => { + (identitiesMap: Record, account) => { identitiesMap[account.address] = { address: account.address, name: account.metadata.name, diff --git a/app/scripts/controllers/preferences.d.ts b/app/scripts/controllers/preferences.d.ts deleted file mode 100644 index cc6a99af3671..000000000000 --- a/app/scripts/controllers/preferences.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type AccountIdentityEntry = { - address: string; - name: string; -}; - -export type PreferencesControllerState = { - identities: { [address: string]: AccountIdentityEntry }; - securityAlertsEnabled: boolean; -}; - -export type PreferencesController = { - setSelectedAddress(addressToLowerCase: string): void; - getSelectedAddress(): string; - setAccountLabel(address: string, label: string): void; - store: { - getState: () => PreferencesControllerState; - subscribe: (callback: (state: PreferencesControllerState) => void) => void; - }; -}; diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 76bc5daaf88c..d623d4868af5 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -11,7 +11,7 @@ import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; -import { PreferencesController } from '../../controllers/preferences'; +import PreferencesController from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b5d370599ca1..50beb316faf5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -283,7 +283,7 @@ import { NetworkOrderController } from './controllers/network-order'; import { AccountOrderController } from './controllers/account-order'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { isStreamWritable, setupMultiplex } from './lib/stream-utils'; -import PreferencesController from './controllers/preferences'; +import PreferencesController from './controllers/preferences-controller'; import AppStateController from './controllers/app-state'; import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; @@ -328,6 +328,7 @@ import { addDappTransaction, addTransaction } from './lib/transaction/util'; import { LatticeKeyringOffscreen } from './lib/offscreen-bridge/lattice-offscreen-keyring'; import PREINSTALLED_SNAPS from './snaps/preinstalled-snaps'; import { WeakRefObjectMap } from './lib/WeakRefObjectMap'; +import { METAMASK_COOKIE_HANDLER } from './constants/stream'; // Notification controllers import { createTxVerificationMiddleware } from './lib/tx-verification/tx-verification-middleware'; @@ -399,6 +400,8 @@ export default class MetamaskController extends EventEmitter { this.loggingController = new LoggingController({ messenger: this.controllerMessenger.getRestricted({ name: 'LoggingController', + allowedActions: [], + allowedEvents: [], }), state: initState.LoggingController, }); @@ -3203,6 +3206,10 @@ export default class MetamaskController extends EventEmitter { metaMetricsController.setDataCollectionForMarketing.bind( metaMetricsController, ), + setMarketingCampaignCookieId: + metaMetricsController.setMarketingCampaignCookieId.bind( + metaMetricsController, + ), setCurrentLocale: preferencesController.setCurrentLocale.bind( preferencesController, ), @@ -5120,6 +5127,42 @@ export default class MetamaskController extends EventEmitter { ); } + setUpCookieHandlerCommunication({ connectionStream }) { + const { + metaMetricsId, + dataCollectionForMarketing, + participateInMetaMetrics, + } = this.metaMetricsController.store.getState(); + + if ( + metaMetricsId && + dataCollectionForMarketing && + participateInMetaMetrics + ) { + // setup multiplexing + const mux = setupMultiplex(connectionStream); + const metamaskCookieHandlerStream = mux.createStream( + METAMASK_COOKIE_HANDLER, + ); + // set up postStream transport + metamaskCookieHandlerStream.on( + 'data', + createMetaRPCHandler( + { + getCookieFromMarketingPage: + this.getCookieFromMarketingPage.bind(this), + }, + metamaskCookieHandlerStream, + ), + ); + } + } + + getCookieFromMarketingPage(data) { + const { ga_client_id: cookieId } = data; + this.metaMetricsController.setMarketingCampaignCookieId(cookieId); + } + /** * Called when we detect a suspicious domain. Requests the browser redirects * to our anti-phishing page. diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index bae372187f14..72fc4ef81e26 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -115,11 +115,11 @@ const rpcMethodMiddlewareMock = { jest.mock('./lib/rpc-method-middleware', () => rpcMethodMiddlewareMock); jest.mock( - './controllers/preferences', + './controllers/preferences-controller', () => function (...args) { const PreferencesController = jest.requireActual( - './controllers/preferences', + './controllers/preferences-controller', ).default; const controller = new PreferencesController(...args); // jest.spyOn gets hoisted to the top of this function before controller is initialized. diff --git a/app/scripts/migrations/109.ts b/app/scripts/migrations/109.ts index 13e268b8e061..f65ec111e835 100644 --- a/app/scripts/migrations/109.ts +++ b/app/scripts/migrations/109.ts @@ -1,6 +1,6 @@ import { cloneDeep, isEmpty } from 'lodash'; import { FALLBACK_VARIATION, NameOrigin } from '@metamask/name-controller'; -import { PreferencesControllerState } from '../controllers/preferences'; +import { PreferencesControllerState } from '../controllers/preferences-controller'; type VersionedData = { meta: { version: number }; diff --git a/app/scripts/streams/cookie-handler-stream.ts b/app/scripts/streams/cookie-handler-stream.ts new file mode 100644 index 000000000000..1b218e8d0cec --- /dev/null +++ b/app/scripts/streams/cookie-handler-stream.ts @@ -0,0 +1,193 @@ +import browser from 'webextension-polyfill'; +import { WindowPostMessageStream } from '@metamask/post-message-stream'; +import ObjectMultiplex from '@metamask/object-multiplex'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error types/readable-stream.d.ts does not get picked up by ts-node +import { pipeline } from 'readable-stream'; +import { Substream } from '@metamask/object-multiplex/dist/Substream'; +import PortStream from 'extension-port-stream'; +import { EXTENSION_MESSAGES } from '../../../shared/constants/app'; +import { COOKIE_ID_MARKETING_WHITELIST_ORIGINS } from '../constants/marketing-site-whitelist'; +import { checkForLastError } from '../../../shared/modules/browser-runtime.utils'; +import { + METAMASK_COOKIE_HANDLER, + CONTENT_SCRIPT, + LEGACY_PUBLIC_CONFIG, + METAMASK_PROVIDER, + PHISHING_SAFELIST, + LEGACY_PROVIDER, + PHISHING_STREAM, +} from '../constants/stream'; +import { logStreamDisconnectWarning } from './shared'; + +export const isDetectedCookieMarketingSite: boolean = + COOKIE_ID_MARKETING_WHITELIST_ORIGINS.some( + (origin) => origin === window.location.origin, + ); + +let cookieHandlerPageMux: ObjectMultiplex, + cookieHandlerPageChannel: Substream, + cookieHandlerExtPort: browser.Runtime.Port, + cookieHandlerExtStream: PortStream | null, + cookieHandlerMux: ObjectMultiplex, + cookieHandlerExtChannel: Substream; + +function setupCookieHandlerStreamsFromOrigin(origin: string): void { + const cookieHandlerPageStream = new WindowPostMessageStream({ + name: CONTENT_SCRIPT, + target: 'CookieHandlerPage', + targetWindow: window, + targetOrigin: origin, + }); + + // create and connect channel muxers + // so we can handle the channels individually + cookieHandlerPageMux = new ObjectMultiplex(); + cookieHandlerPageMux.setMaxListeners(25); + + pipeline( + cookieHandlerPageMux, + cookieHandlerPageStream, + cookieHandlerPageMux, + (err: Error) => + logStreamDisconnectWarning('MetaMask Inpage Multiplex', err), + ); + + cookieHandlerPageChannel = cookieHandlerPageMux.createStream( + METAMASK_COOKIE_HANDLER, + ); + cookieHandlerPageMux.ignoreStream(LEGACY_PUBLIC_CONFIG); + cookieHandlerPageMux.ignoreStream(LEGACY_PROVIDER); + cookieHandlerPageMux.ignoreStream(METAMASK_PROVIDER); + cookieHandlerPageMux.ignoreStream(PHISHING_SAFELIST); + cookieHandlerPageMux.ignoreStream(PHISHING_STREAM); +} + +/** + * establishes a communication stream between the content script and background.js + */ +export const setupCookieHandlerExtStreams = (): void => { + cookieHandlerExtPort = browser.runtime.connect({ + name: CONTENT_SCRIPT, + }); + cookieHandlerExtStream = new PortStream(cookieHandlerExtPort); + + // create and connect channel muxers + // so we can handle the channels individually + cookieHandlerMux = new ObjectMultiplex(); + cookieHandlerMux.setMaxListeners(25); + + pipeline( + cookieHandlerMux, + cookieHandlerExtStream, + cookieHandlerMux, + (err: Error) => { + logStreamDisconnectWarning('MetaMask Background Multiplex', err); + window.postMessage( + { + target: 'CookieHandlerPage', + data: { + // this object gets passed to @metamask/object-multiplex + name: METAMASK_COOKIE_HANDLER, // the @metamask/object-multiplex channel name + data: { + jsonrpc: '2.0', + method: 'METAMASK_STREAM_FAILURE', + }, + }, + }, + window.location.origin, + ); + }, + ); + + // forward communication across inpage-background for these channels only + cookieHandlerExtChannel = cookieHandlerMux.createStream( + METAMASK_COOKIE_HANDLER, + ); + cookieHandlerMux.ignoreStream(LEGACY_PUBLIC_CONFIG); + cookieHandlerMux.ignoreStream(LEGACY_PROVIDER); + cookieHandlerMux.ignoreStream(METAMASK_PROVIDER); + cookieHandlerMux.ignoreStream(PHISHING_SAFELIST); + cookieHandlerMux.ignoreStream(PHISHING_STREAM); + pipeline( + cookieHandlerPageChannel, + cookieHandlerExtChannel, + cookieHandlerPageChannel, + (error: Error) => + console.debug( + `MetaMask: Muxed traffic for channel "${METAMASK_COOKIE_HANDLER}" failed.`, + error, + ), + ); + + cookieHandlerExtPort.onDisconnect.addListener( + // eslint-disable-next-line @typescript-eslint/no-use-before-define + onDisconnectDestroyCookieStreams, + ); +}; + +/** Destroys all of the cookie handler extension streams */ +const destroyCookieExtStreams = () => { + cookieHandlerPageChannel.removeAllListeners(); + + cookieHandlerMux.removeAllListeners(); + cookieHandlerMux.destroy(); + + cookieHandlerExtChannel.removeAllListeners(); + cookieHandlerExtChannel.destroy(); + + cookieHandlerExtStream = null; +}; + +/** + * This listener destroys the phishing extension streams when the extension port is disconnected, + * so that streams may be re-established later the phishing extension port is reconnected. + */ +const onDisconnectDestroyCookieStreams = () => { + const err = checkForLastError(); + + cookieHandlerExtPort.onDisconnect.removeListener( + onDisconnectDestroyCookieStreams, + ); + + destroyCookieExtStreams(); + + /** + * If an error is found, reset the streams. When running two or more dapps, resetting the service + * worker may cause the error, "Error: Could not establish connection. Receiving end does not + * exist.", due to a race-condition. The disconnect event may be called by runtime.connect which + * may cause issues. We suspect that this is a chromium bug as this event should only be called + * once the port and connections are ready. Delay time is arbitrary. + */ + if (err) { + console.warn(`${err} Resetting the phishing streams.`); + setTimeout(setupCookieHandlerExtStreams, 1000); + } +}; + +const onMessageSetUpCookieHandlerStreams = (msg: { + name: string; + origin: string; +}): Promise | undefined => { + if (msg.name === EXTENSION_MESSAGES.READY) { + if (!cookieHandlerExtStream) { + setupCookieHandlerExtStreams(); + } + return Promise.resolve( + `MetaMask: handled "${EXTENSION_MESSAGES.READY}" for phishing streams`, + ); + } + return undefined; +}; + +/** + * Initializes two-way communication streams between the browser extension and + * the cookie id submission page context. This function also creates an event listener to + * reset the streams if the service worker resets. + */ +export const initializeCookieHandlerSteam = (): void => { + const { origin } = window.location; + setupCookieHandlerStreamsFromOrigin(origin); + setupCookieHandlerExtStreams(); + browser.runtime.onMessage.addListener(onMessageSetUpCookieHandlerStreams); +}; diff --git a/app/scripts/streams/shared.ts b/app/scripts/streams/shared.ts new file mode 100644 index 000000000000..73dc6ec1d6e5 --- /dev/null +++ b/app/scripts/streams/shared.ts @@ -0,0 +1,15 @@ +/** + * Error handler for page to extension stream disconnections + * + * @param remoteLabel - Remote stream name + * @param error - Stream connection error + */ +export function logStreamDisconnectWarning( + remoteLabel: string, + error: Error, +): void { + console.debug( + `MetaMask: Content script lost connection to "${remoteLabel}".`, + error, + ); +} diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 5a53f4a118e0..aebbd076f07b 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -621,7 +621,7 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, @@ -629,14 +629,6 @@ "uuid": true } }, - "@metamask/accounts-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, @@ -1488,18 +1480,10 @@ }, "@metamask/logging-controller": { "packages": { - "@metamask/logging-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "uuid": true } }, - "@metamask/logging-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/logo": { "globals": { "addEventListener": true, @@ -2297,22 +2281,14 @@ "console.info": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/logging-controller": true, "@metamask/message-manager": true, - "@metamask/signature-controller>@metamask/base-controller": true, "lodash": true, "webpack>events": true } }, - "@metamask/signature-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/smart-transactions-controller": { "globals": { "URLSearchParams": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 5a53f4a118e0..aebbd076f07b 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -621,7 +621,7 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, @@ -629,14 +629,6 @@ "uuid": true } }, - "@metamask/accounts-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, @@ -1488,18 +1480,10 @@ }, "@metamask/logging-controller": { "packages": { - "@metamask/logging-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "uuid": true } }, - "@metamask/logging-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/logo": { "globals": { "addEventListener": true, @@ -2297,22 +2281,14 @@ "console.info": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/logging-controller": true, "@metamask/message-manager": true, - "@metamask/signature-controller>@metamask/base-controller": true, "lodash": true, "webpack>events": true } }, - "@metamask/signature-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/smart-transactions-controller": { "globals": { "URLSearchParams": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 5a53f4a118e0..aebbd076f07b 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -621,7 +621,7 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, @@ -629,14 +629,6 @@ "uuid": true } }, - "@metamask/accounts-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, @@ -1488,18 +1480,10 @@ }, "@metamask/logging-controller": { "packages": { - "@metamask/logging-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "uuid": true } }, - "@metamask/logging-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/logo": { "globals": { "addEventListener": true, @@ -2297,22 +2281,14 @@ "console.info": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/logging-controller": true, "@metamask/message-manager": true, - "@metamask/signature-controller>@metamask/base-controller": true, "lodash": true, "webpack>events": true } }, - "@metamask/signature-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/smart-transactions-controller": { "globals": { "URLSearchParams": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index ab7f80e77aa4..3077fa02e339 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -713,7 +713,7 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, @@ -721,14 +721,6 @@ "uuid": true } }, - "@metamask/accounts-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, @@ -1580,18 +1572,10 @@ }, "@metamask/logging-controller": { "packages": { - "@metamask/logging-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "uuid": true } }, - "@metamask/logging-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/logo": { "globals": { "addEventListener": true, @@ -2389,22 +2373,14 @@ "console.info": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/logging-controller": true, "@metamask/message-manager": true, - "@metamask/signature-controller>@metamask/base-controller": true, "lodash": true, "webpack>events": true } }, - "@metamask/signature-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/smart-transactions-controller": { "globals": { "URLSearchParams": true, diff --git a/package.json b/package.json index 1ce60ff7fff0..b007a704c8e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.1.2", + "version": "12.2.2", "private": true, "repository": { "type": "git", @@ -300,7 +300,7 @@ "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", "@metamask/account-watcher": "^4.1.0", - "@metamask/accounts-controller": "^18.1.0", + "@metamask/accounts-controller": "^18.2.0", "@metamask/address-book-controller": "^5.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", @@ -329,7 +329,7 @@ "@metamask/jazzicon": "^2.0.0", "@metamask/keyring-api": "^8.0.0", "@metamask/keyring-controller": "patch:@metamask/keyring-controller@npm%3A17.1.1#~/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch", - "@metamask/logging-controller": "^5.0.0", + "@metamask/logging-controller": "^6.0.0", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^10.1.0", "@metamask/message-signing-snap": "^0.3.3", @@ -353,7 +353,7 @@ "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^15.0.2", - "@metamask/signature-controller": "^18.1.0", + "@metamask/signature-controller": "^19.0.0", "@metamask/smart-transactions-controller": "^13.0.0", "@metamask/snaps-controllers": "^9.5.0", "@metamask/snaps-execution-environments": "^6.6.2", @@ -655,7 +655,6 @@ "source-map-explorer": "^2.4.2", "sprintf-js": "^1.1.3", "storybook": "^7.6.20", - "storybook-dark-mode": "^4.0.2", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string.prototype.matchall": "^4.0.2", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index be5adda68c08..ea50d7a5f9fd 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -556,6 +556,7 @@ export enum MetaMetricsEventName { NavBuyButtonClicked = 'Buy Button Clicked', NavSendButtonClicked = 'Send Button Clicked', NavSwapButtonClicked = 'Swap Button Clicked', + NavReceiveButtonClicked = 'Receive Button Clicked', NftAdded = 'NFT Added', OnboardingWalletCreationStarted = 'Wallet Setup Selected', OnboardingWalletImportStarted = 'Wallet Import Started', diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 5caa775a9518..67123b83d5d7 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -4,7 +4,7 @@ import { TransactionUpdateController } from '@metamask-institutional/transaction import { CustodyController } from '@metamask-institutional/custody-controller'; import { SignatureController } from '@metamask/signature-controller'; import { NetworkController } from '@metamask/network-controller'; -import { PreferencesController } from '../../app/scripts/controllers/preferences'; +import PreferencesController from '../../app/scripts/controllers/preferences-controller'; import { AppStateController } from '../../app/scripts/controllers/app-state'; import AccountTracker from '../../app/scripts/lib/account-tracker'; import MetaMetricsController from '../../app/scripts/controllers/metametrics'; diff --git a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts index 52cb4f976641..60a141144833 100644 --- a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts @@ -114,7 +114,7 @@ async function mocks(server: MockttpServer) { return [await mocked4Bytes(server)]; } -async function importTST(driver: Driver) { +export async function importTST(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); await driver.clickElement('[data-testid="import-token-button"]'); @@ -145,7 +145,7 @@ async function importTST(driver: Driver) { }); } -async function createERC20ApproveTransaction(driver: Driver) { +export async function createERC20ApproveTransaction(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#approveTokens'); } @@ -213,7 +213,8 @@ async function assertApproveDetails(driver: Driver) { }); } -async function confirmApproveTransaction(driver: Driver) { +export async function confirmApproveTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await scrollAndConfirmAndAssertConfirm(driver); await driver.delay(veryLargeDelayMs); diff --git a/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts new file mode 100644 index 000000000000..2571a69107b3 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts @@ -0,0 +1,200 @@ +import FixtureBuilder from '../../../fixture-builder'; +import { + defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + largeDelayMs, + veryLargeDelayMs, + WINDOW_TITLES, + withFixtures, +} from '../../../helpers'; +import { Mockttp } from '../../../mock-e2e'; +import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; +import { SMART_CONTRACTS } from '../../../seeder/smart-contracts'; +import { Driver } from '../../../webdriver/driver'; +import { scrollAndConfirmAndAssertConfirm } from '../helpers'; +import { openDAppWithContract, TestSuiteArguments } from './shared'; + +describe('Confirmation Redesign ERC20 Increase Allowance', function () { + describe('Submit an increase allowance transaction @no-mmi', function () { + it('Sends a type 0 transaction (Legacy) with a small spending cap', async function () { + await withFixtures( + generateFixtureOptionsForLegacyTx(this), + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createAndAssertIncreaseAllowanceSubmission( + driver, + '3', + contractRegistry, + ); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559) with a small spending cap', async function () { + await withFixtures( + generateFixtureOptionsForEIP1559Tx(this), + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createAndAssertIncreaseAllowanceSubmission( + driver, + '3', + contractRegistry, + ); + }, + ); + }); + + it('Sends a type 0 transaction (Legacy) with a large spending cap', async function () { + await withFixtures( + generateFixtureOptionsForLegacyTx(this), + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createAndAssertIncreaseAllowanceSubmission( + driver, + '3000', + contractRegistry, + ); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559) with a large spending cap', async function () { + await withFixtures( + generateFixtureOptionsForEIP1559Tx(this), + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createAndAssertIncreaseAllowanceSubmission( + driver, + '3000', + contractRegistry, + ); + }, + ); + }); + }); +}); + +function generateFixtureOptionsForLegacyTx(mochaContext: Mocha.Context) { + return { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract: SMART_CONTRACTS.HST, + testSpecificMock: mocks, + title: mochaContext.test?.fullTitle(), + }; +} + +function generateFixtureOptionsForEIP1559Tx(mochaContext: Mocha.Context) { + return { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract: SMART_CONTRACTS.HST, + testSpecificMock: mocks, + title: mochaContext.test?.fullTitle(), + }; +} + +async function mocks(server: Mockttp) { + return [await mocked4BytesIncreaseAllowance(server)]; +} + +async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { + return await mockServer + .forGet('https://www.4byte.directory/api/v1/signatures/') + .always() + .withQuery({ hex_signature: '0x39509351' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + count: 1, + next: null, + previous: null, + results: [ + { + id: 46002, + created_at: '2018-06-24T21:43:27.354648Z', + text_signature: 'increaseAllowance(address,uint256)', + hex_signature: '0x39509351', + bytes_signature: '9P“Q', + test: 'Priya', + }, + ], + }, + }; + }); +} + +async function createAndAssertIncreaseAllowanceSubmission( + driver: Driver, + newSpendingCap: string, + contractRegistry?: GanacheContractAddressRegistry, +) { + await openDAppWithContract(driver, contractRegistry, SMART_CONTRACTS.HST); + + await createERC20IncreaseAllowanceTransaction(driver); + + await editSpendingCap(driver, newSpendingCap); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, newSpendingCap); +} + +async function createERC20IncreaseAllowanceTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#increaseTokenAllowance'); +} + +async function editSpendingCap(driver: Driver, newSpendingCap: string) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement('[data-testid="edit-spending-cap-icon"'); + + await driver.fill( + '[data-testid="custom-spending-cap-input"]', + newSpendingCap, + ); + + await driver.delay(largeDelayMs); + + await driver.clickElement({ text: 'Save', tag: 'button' }); + + // wait for the confirmation to be updated before submitting tx + await driver.delay(veryLargeDelayMs * 2); +} + +async function assertChangedSpendingCap( + driver: Driver, + newSpendingCap: string, +) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + await driver.clickElement({ text: 'Activity', tag: 'button' }); + + await driver.delay(veryLargeDelayMs); + + await driver.clickElement( + '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', + ); + + await driver.waitForSelector({ + text: `${newSpendingCap} TST`, + tag: 'span', + }); + + await driver.waitForSelector({ text: 'Confirmed', tag: 'div' }); +} diff --git a/test/e2e/tests/metrics/marketing-cookieid-mock-page/index.html b/test/e2e/tests/metrics/marketing-cookieid-mock-page/index.html new file mode 100644 index 000000000000..9233e3bf6a34 --- /dev/null +++ b/test/e2e/tests/metrics/marketing-cookieid-mock-page/index.html @@ -0,0 +1,35 @@ + + + + Mock E2E Cookie Handler + + + +
Mock Page for E2E Cookie Handler
+