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