From d90c41d835a459e580bf043a710e2dd21a160d93 Mon Sep 17 00:00:00 2001 From: Christian Tran Date: Wed, 13 May 2026 16:52:59 +0200 Subject: [PATCH 1/5] fix(corebackend): update HTTP headers (#8798) ## Explanation ## References ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Changes request header names and makes `clientVersion` optional, which could break backend compatibility or analytics if consumers/backends still expect the old headers or a default version. > > **Overview** > `BaseApiClient` now sends `x-metamask-clientproduct` and (optionally) `x-metamask-clientversion` instead of the previous `X-Client-Product`/`X-Client-Version` headers, and it no longer defaults `clientVersion` to `1.0.0`. > > Tests and the `core-backend` changelog are updated to reflect the new header contract, including verifying the version header is *omitted* when `clientVersion` is not provided. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 09cc23a5fcec2d62293a496491bc580c55148a4b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/core-backend/CHANGELOG.md | 5 +++++ .../core-backend/src/api/ApiPlatformClient.test.ts | 10 +++++----- packages/core-backend/src/api/base-client.ts | 11 +++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core-backend/CHANGELOG.md b/packages/core-backend/CHANGELOG.md index f371a1c977..eb49833023 100644 --- a/packages/core-backend/CHANGELOG.md +++ b/packages/core-backend/CHANGELOG.md @@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) - Bump `@metamask/profile-sync-controller` from `^28.0.2` to `^28.1.0` ([#8783](https://github.com/MetaMask/core/pull/8783)) +### Fixed + +- Update HTTP headers from `X-Client-Product`/`X-Client-Version` to `x-metamask-clientproduct`/`x-metamask-clientversion` ([#8798](https://github.com/MetaMask/core/pull/8798)) +- Remove default `clientVersion` value of `1.0.0`; the `x-metamask-clientversion` header is now only sent when `clientVersion` is explicitly provided ([#8798](https://github.com/MetaMask/core/pull/8798)) + ## [6.2.2] ### Changed diff --git a/packages/core-backend/src/api/ApiPlatformClient.test.ts b/packages/core-backend/src/api/ApiPlatformClient.test.ts index b991b6fbed..397193c6a3 100644 --- a/packages/core-backend/src/api/ApiPlatformClient.test.ts +++ b/packages/core-backend/src/api/ApiPlatformClient.test.ts @@ -91,7 +91,7 @@ describe('ApiPlatformClient', () => { expect(instance.tokens.queryClient).toBe(customQueryClient); }); - it('uses default version when not provided', async () => { + it('omits clientversion header when not provided', async () => { const instance = new ApiPlatformClient({ clientProduct: 'test-client', queryClient: new QueryClient({ @@ -108,8 +108,8 @@ describe('ApiPlatformClient', () => { expect(mockFetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - headers: expect.objectContaining({ - 'X-Client-Version': '1.0.0', + headers: expect.not.objectContaining({ + 'x-metamask-clientversion': expect.any(String), }), }), ); @@ -145,8 +145,8 @@ describe('ApiPlatformClient', () => { method: 'GET', headers: { 'Content-Type': 'application/json', - 'X-Client-Product': 'test-client', - 'X-Client-Version': '1.0.0', + 'x-metamask-clientproduct': 'test-client', + 'x-metamask-clientversion': '1.0.0', }, }), ); diff --git a/packages/core-backend/src/api/base-client.ts b/packages/core-backend/src/api/base-client.ts index 00054532c4..c8a1d1e01a 100644 --- a/packages/core-backend/src/api/base-client.ts +++ b/packages/core-backend/src/api/base-client.ts @@ -40,7 +40,7 @@ export type InternalFetchOptions = { export class BaseApiClient { protected readonly clientProduct: string; - protected readonly clientVersion: string; + protected readonly clientVersion?: string; protected readonly getBearerToken?: () => Promise; @@ -71,7 +71,7 @@ export class BaseApiClient { constructor(options: ApiPlatformClientOptions) { this.clientProduct = options.clientProduct; - this.clientVersion = options.clientVersion ?? '1.0.0'; + this.clientVersion = options.clientVersion; this.getBearerToken = options.getBearerToken; this.#queryClientInstance = @@ -121,10 +121,13 @@ export class BaseApiClient { const headers: Record = { 'Content-Type': 'application/json', - 'X-Client-Product': this.clientProduct, - 'X-Client-Version': this.clientVersion, + 'x-metamask-clientproduct': this.clientProduct, }; + if (this.clientVersion) { + headers['x-metamask-clientversion'] = this.clientVersion; + } + // Get bearer token using fetchQuery for automatic deduplication if (this.getBearerToken) { const queryKey = authQueryKeys.bearerToken(); From 4d2e89fe7f87fd41f5913e459b2fab1bf77d3f88 Mon Sep 17 00:00:00 2001 From: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Date: Wed, 13 May 2026 17:39:36 +0200 Subject: [PATCH 2/5] feat: implement rich notification setting config using AUS (#8784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation This PR updates notification preferences to use AUS as the source of truth after first initialization, in order to enrich notification settings with the following categories: - Updates and Rewards (or marketing) - Wallet Activity - Perps - Social AI For new users, the NotificationServicesController now writes a complete preferences blob, seeding wallet activity from the current Trigger API state and defaulting all current accounts to enabled for true first-time setup. Marketing initialization is split by channel: push follows marketing consent, while in-app follows the product announcement current option. ## References Fixes [GE-13](https://consensyssoftware.atlassian.net/browse/GE-13) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [x] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Changes the notification settings source of truth and initialization/write paths (Trigger API -> AUS), which can affect user notification enablement and push registration behavior across accounts and channels. > > **Overview** > **Moves notification preference storage to Authenticated User Storage (AUS).** `NotificationServicesController` now reads/writes notification preferences via AUS messenger actions and uses those preferences (instead of Trigger API config) when enabling push notifications, checking account presence, and selecting addresses for fetching on-chain notifications. > > **Adds first-time preference initialization and new options.** When AUS has no preferences (`null`), `createOnChainTriggers` writes a complete preferences blob (wallet activity seeded from current Trigger API state with first-time “enable all” fallback, plus default Perps/SocialAI) and seeds marketing push/in-app from new `hasMarketingConsent` and `productAnnouncementEnabled` options; the old `resetNotifications` option is removed. > > **Updates shared types and tests.** `@metamask/authenticated-user-storage` notification preference types/validators replace `enabled` with per-channel `inAppNotificationsEnabled`/`pushNotificationsEnabled`, and notification-services-controller tests/mocks are rewritten to mock AUS calls and drop `updateOnChainNotifications` coverage. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c1c039ddeb41a502f151e74d0579d7be20de4dd6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). [GE-13]: https://consensyssoftware.atlassian.net/browse/GE-13?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- README.md | 1 + .../authenticated-user-storage/CHANGELOG.md | 1 + .../authenticated-user-storage/src/types.ts | 12 +- .../src/validators.ts | 12 +- .../tests/mocks/authenticated-userstorage.ts | 16 +- .../CHANGELOG.md | 15 +- .../package.json | 1 + ...nServicesController-method-action-types.ts | 11 +- .../NotificationServicesController.test.ts | 752 +++++++++++------- .../NotificationServicesController.ts | 344 ++++++-- .../__fixtures__/mockServices.ts | 14 - .../mocks/mockResponses.ts | 23 +- .../services/api-notifications.test.ts | 57 -- .../services/api-notifications.ts | 49 +- .../src/index.ts | 4 + .../tsconfig.build.json | 3 + .../tsconfig.json | 3 + yarn.lock | 1 + 18 files changed, 803 insertions(+), 516 deletions(-) diff --git a/README.md b/README.md index 8dd6a4beef..a44a28354a 100644 --- a/README.md +++ b/README.md @@ -430,6 +430,7 @@ linkStyle default opacity:0.5 network_enablement_controller --> multichain_network_controller; network_enablement_controller --> network_controller; network_enablement_controller --> transaction_controller; + notification_services_controller --> authenticated_user_storage; notification_services_controller --> base_controller; notification_services_controller --> controller_utils; notification_services_controller --> keyring_controller; diff --git a/packages/authenticated-user-storage/CHANGELOG.md b/packages/authenticated-user-storage/CHANGELOG.md index c32b3fca7b..0c439f88c3 100644 --- a/packages/authenticated-user-storage/CHANGELOG.md +++ b/packages/authenticated-user-storage/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) +- **BREAKING:** Replace `enabled` by `inAppNotificationsEnabled` and `pushNotificationsEnabled` in all the `NotificationPreferences` type fields and validation to match the API payload. ([#8784](https://github.com/MetaMask/core/pull/8784)) ## [1.0.1] diff --git a/packages/authenticated-user-storage/src/types.ts b/packages/authenticated-user-storage/src/types.ts index e6087eb125..e263160d12 100644 --- a/packages/authenticated-user-storage/src/types.ts +++ b/packages/authenticated-user-storage/src/types.ts @@ -70,12 +70,14 @@ export type WalletActivityAccount = { }; export type WalletActivityPreference = { - enabled: boolean; + inAppNotificationsEnabled: boolean; + pushNotificationsEnabled: boolean; accounts: WalletActivityAccount[]; }; export type MarketingPreference = { - enabled: boolean; + inAppNotificationsEnabled: boolean; + pushNotificationsEnabled: boolean; }; export type PerpsWatchlistExchange = { @@ -89,12 +91,14 @@ export type PerpsWatchlistMarkets = { }; export type PerpsPreference = { - enabled: boolean; + inAppNotificationsEnabled: boolean; + pushNotificationsEnabled: boolean; watchlistMarkets?: PerpsWatchlistMarkets; }; export type SocialAIPreference = { - enabled: boolean; + inAppNotificationsEnabled: boolean; + pushNotificationsEnabled: boolean; txAmountLimit?: number; mutedTraderProfileIds: string[]; }; diff --git a/packages/authenticated-user-storage/src/validators.ts b/packages/authenticated-user-storage/src/validators.ts index a1703822d3..e9bef8850c 100644 --- a/packages/authenticated-user-storage/src/validators.ts +++ b/packages/authenticated-user-storage/src/validators.ts @@ -54,12 +54,14 @@ const WalletActivityAccountSchema = type({ }); const WalletActivityPreferenceSchema = type({ - enabled: boolean(), + inAppNotificationsEnabled: boolean(), + pushNotificationsEnabled: boolean(), accounts: array(WalletActivityAccountSchema), }); const MarketingPreferenceSchema = type({ - enabled: boolean(), + inAppNotificationsEnabled: boolean(), + pushNotificationsEnabled: boolean(), }); const PerpsWatchlistExchangeSchema = type({ @@ -73,12 +75,14 @@ const PerpsWatchlistMarketsSchema = type({ }); const PerpsPreferenceSchema = type({ - enabled: boolean(), + inAppNotificationsEnabled: boolean(), + pushNotificationsEnabled: boolean(), watchlistMarkets: optional(PerpsWatchlistMarketsSchema), }); const SocialAIPreferenceSchema = type({ - enabled: boolean(), + inAppNotificationsEnabled: boolean(), + pushNotificationsEnabled: boolean(), txAmountLimit: optional(number()), mutedTraderProfileIds: array(string()), }); diff --git a/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts b/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts index 71569c9351..39f42beb72 100644 --- a/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts +++ b/packages/authenticated-user-storage/tests/mocks/authenticated-userstorage.ts @@ -40,7 +40,8 @@ export const MOCK_DELEGATION_RESPONSE: DelegationResponse = export const MOCK_NOTIFICATION_PREFERENCES: NotificationPreferences = { walletActivity: { - enabled: true, + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, accounts: [ { address: '0x1234567890abcdef1234567890abcdef12345678', @@ -48,10 +49,17 @@ export const MOCK_NOTIFICATION_PREFERENCES: NotificationPreferences = { }, ], }, - marketing: { enabled: false }, - perps: { enabled: true }, + marketing: { + inAppNotificationsEnabled: false, + pushNotificationsEnabled: false, + }, + perps: { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + }, socialAI: { - enabled: true, + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, txAmountLimit: 100, mutedTraderProfileIds: [ 'b3a7c9d1-4e2f-4a8b-9c6d-1f2e3a4b5c6d', diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 8e3ce5ae25..ddf16704d3 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,11 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `productAnnouncementEnabled` to `NotificationServicesControllerEnableNotificationsOptions`. ([#8784](https://github.com/MetaMask/core/pull/8784)) + ### Changed +- **BREAKING:** Enrich notification settings using Authenticated User Storage. ([#8784](https://github.com/MetaMask/core/pull/8784)) + - Replace Trigger API notification settings with AUS notification preferences as the source of truth. + - `NotificationServicesController` now requires AUS messenger actions for notification setup. + - When AUS has no stored preferences, `createOnChainTriggers` writes a complete preferences blob for `walletActivity`, `marketing`, `perps`, and `socialAI`. + - Wallet activity accounts are seeded from the current Trigger API config when at least one current account is already enabled; otherwise all current accounts are initialized as enabled for first-time notification setup. + - Marketing push notifications are initialized from `hasMarketingConsent`, while marketing in-app notifications are initialized from `productAnnouncementEnabled`. - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) - Bump `@metamask/profile-sync-controller` from `^28.0.2` to `^28.1.0` ([#8783](https://github.com/MetaMask/core/pull/8783)) +### Removed + +- **BREAKING:** Remove unused `resetNotifications` option from `NotificationServicesControllerEnableNotificationsOptions`. ([#8784](https://github.com/MetaMask/core/pull/8784)) + ## [23.1.1] ### Changed @@ -295,7 +309,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed method `updateOnChainTriggersByAccount` to `enableAccounts` in `NotificationServicesController` - Renamed method `deleteOnChainTriggersByAccount` to `disableAccounts` in `NotificationServicesController` - Deprecated `updateTriggerPushNotifications` from `NotificationServicesPushController` and will be removed in a subsequent release. - - Bump `@metamask/controller-utils` to `^11.10.0` ([#5935](https://github.com/MetaMask/core/pull/5935)) ### Removed diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index ccd44352c4..615bbe5186 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -106,6 +106,7 @@ }, "dependencies": { "@contentful/rich-text-html-renderer": "^16.5.2", + "@metamask/authenticated-user-storage": "^1.0.1", "@metamask/base-controller": "^9.1.0", "@metamask/controller-utils": "^12.1.0", "@metamask/keyring-controller": "^25.5.0", diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts index 041f8ea552..df3a4ff636 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController-method-action-types.ts @@ -51,9 +51,15 @@ export type NotificationServicesControllerSetFeatureAnnouncementsEnabledAction = * * **Action** - Used during Sign In / Enabling of notifications. * + * Notification preferences are initialized only when + * {@link AuthenticatedUserStorageService} has no stored preferences yet. + * Existing preferences are left as-is. + * * @param opts - optional options to mutate this functionality - * @param opts.resetNotifications - this will not use the users stored preferences, and instead re-create notification triggers - * It will help in case uses get into a corrupted state or wants to wipe their notifications. + * @param opts.hasMarketingConsent - The user's marketing-consent flag. + * Used only during initialization to seed marketing push notifications. + * @param opts.productAnnouncementEnabled - The user's product-announcement flag. + * Used only during initialization to seed marketing in-app notifications. * @returns The updated or newly created user storage. * @throws {Error} Throws an error if unauthenticated or from other operations. */ @@ -66,6 +72,7 @@ export type NotificationServicesControllerCreateOnChainTriggersAction = { * Enables all MetaMask notifications for the user. * This is identical flow when initializing notifications for the first time. * + * @param opts - Optional settings for first-time AUS notification preferences initialization. * @throws {Error} If there is an error during the process of enabling notifications. */ export type NotificationServicesControllerEnableMetamaskNotificationsAction = { diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts index 6d550478b5..1ec067c7c1 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts @@ -1,3 +1,8 @@ +import type { + AuthenticatedUserStorageServiceGetNotificationPreferencesAction, + AuthenticatedUserStorageServicePutNotificationPreferencesAction, + NotificationPreferences, +} from '@metamask/authenticated-user-storage'; import { deriveStateFromMetadata } from '@metamask/base-controller'; import * as ControllerUtils from '@metamask/controller-utils'; import { KeyringTypes } from '@metamask/keyring-controller'; @@ -25,7 +30,6 @@ import type { import { ADDRESS_1, ADDRESS_2, ADDRESS_3 } from './__fixtures__/mockAddresses'; import { mockGetOnChainNotificationsConfig, - mockUpdateOnChainNotifications, mockGetAPINotifications, mockFetchFeatureAnnouncementNotifications, mockMarkNotificationsAsRead, @@ -40,6 +44,8 @@ import { } from './mocks/mock-feature-announcements'; import { createMockNotificationEthSent } from './mocks/mock-raw-notifications'; import { + DEFAULT_PERPS_PREFERENCES, + DEFAULT_SOCIAL_AI_PREFERENCES, NotificationServicesController, ACCOUNTS_UPDATE_DEBOUNCE_TIME_MS, defaultState, @@ -75,7 +81,37 @@ const clearAPICache = (): void => { notificationsConfigCache.clear(); }; +const prefsFromAddresses = ( + accounts: { address: string; enabled: boolean }[], +): NotificationPreferences => ({ + walletActivity: { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + accounts: accounts.map((a) => ({ + address: a.address.toLowerCase() as `0x${string}`, + enabled: a.enabled, + })), + }, + marketing: { + inAppNotificationsEnabled: false, + pushNotificationsEnabled: false, + }, + perps: { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + }, + socialAI: { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + mutedTraderProfileIds: [], + }, +}); + describe('NotificationServicesController', () => { + afterEach(() => { + clearAPICache(); + }); + describe('constructor', () => { it('initializes state & override state', () => { const controller1 = new NotificationServicesController({ @@ -320,11 +356,11 @@ describe('NotificationServicesController', () => { const arrangeActInitialisePushNotifications = ( modifications?: (mocks: ReturnType) => void, ): ReturnType & { - mockAPIGetNotificationConfig: nock.Scope; + mockAPIGetNotificationConfig: jest.Mock; } => { // Arrange - const mockAPIGetNotificationConfig = mockGetOnChainNotificationsConfig(); const mocks = arrangeMocks(); + const mockAPIGetNotificationConfig = mocks.mockGetNotificationPreferences; modifications?.(mocks); // Act @@ -409,17 +445,16 @@ describe('NotificationServicesController', () => { // See /utils for more in-depth testing describe('checkAccountsPresence', () => { it('returns Record with accounts that have notifications enabled', async () => { - const { messenger } = mockNotificationMessenger(); - const mockGetConfig = mockGetOnChainNotificationsConfig({ - status: 200, - body: [ + const mocks = mockNotificationMessenger(); + mocks.mockGetNotificationPreferences.mockResolvedValueOnce( + prefsFromAddresses([ { address: ADDRESS_1, enabled: true }, { address: ADDRESS_2, enabled: false }, - ], - }); + ]), + ); const controller = new NotificationServicesController({ - messenger, + messenger: mocks.messenger, env: { featureAnnouncements: featureAnnouncementsEnv }, }); const result = await controller.checkAccountsPresence([ @@ -427,7 +462,7 @@ describe('NotificationServicesController', () => { ADDRESS_2, ]); - expect(mockGetConfig.isDone()).toBe(true); + expect(mocks.mockGetNotificationPreferences).toHaveBeenCalled(); expect(result).toStrictEqual({ [ADDRESS_1]: true, [ADDRESS_2]: false, @@ -454,15 +489,16 @@ describe('NotificationServicesController', () => { describe('createOnChainTriggers', () => { const arrangeMocks = (overrides?: { - mockGetConfig: () => nock.Scope; + configurePrefs?: (mock: jest.Mock) => void; }): ReturnType & { - mockGetConfig: nock.Scope; - mockUpdateNotifications: nock.Scope; + mockGetConfig: jest.Mock; + mockUpdateNotifications: jest.Mock; } => { const messengerMocks = mockNotificationMessenger(); - const mockGetConfig = - overrides?.mockGetConfig() ?? mockGetOnChainNotificationsConfig(); - const mockUpdateNotifications = mockUpdateOnChainNotifications(); + const mockGetConfig = messengerMocks.mockGetNotificationPreferences; + const mockUpdateNotifications = + messengerMocks.mockPutNotificationPreferences; + overrides?.configurePrefs?.(mockGetConfig); return { ...messengerMocks, mockGetConfig, @@ -470,276 +506,369 @@ describe('NotificationServicesController', () => { }; }; - beforeEach(() => { - clearAPICache(); - }); - - it('create new triggers and push notifications if there are no existing notifications', async () => { - const { - messenger, - mockEnablePushNotifications, - mockGetConfig, - mockUpdateNotifications, - } = arrangeMocks({ - // Mock no existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [], - }), - }); - - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, - }); + describe('when AUS preferences are not initialized (preferences are null)', () => { + it('writes a fresh preferences blob using hardcoded defaults, current Trigger API wallet account state, and supplied marketing flags', async () => { + const { + messenger, + mockEnablePushNotifications, + mockGetConfig, + mockUpdateNotifications, + mockKeyringControllerGetState, + } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); - await controller.createOnChainTriggers(); + mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + accounts: [ADDRESS_1, ADDRESS_2], + type: KeyringTypes.hd, + metadata: { id: 'srp-1', name: 'SRP 1' }, + }, + ], + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig({ + status: 200, + body: [ + { address: ADDRESS_1.toLowerCase(), enabled: true }, + { address: ADDRESS_2.toLowerCase(), enabled: false }, + ], + }); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(true); - expect(mockEnablePushNotifications).toHaveBeenCalled(); - }); + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); - it('tracks accounts from all keyrings when creating triggers', async () => { - const { - messenger, - mockGetConfig, - mockUpdateNotifications, - mockKeyringControllerGetState, - } = arrangeMocks({ - // Mock no existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [], - }), - }); + await controller.createOnChainTriggers({ + hasMarketingConsent: true, + productAnnouncementEnabled: false, + }); - mockKeyringControllerGetState.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - accounts: [ADDRESS_1], - type: KeyringTypes.hd, - metadata: { - id: 'srp-1', - name: 'SRP 1', - }, + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockTriggerQuery.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalledTimes(1); + const [writtenPrefs, writtenPlatform] = + mockUpdateNotifications.mock.calls[0]; + expect(writtenPlatform).toBe(featureAnnouncementsEnv.platform); + expect(writtenPrefs).toStrictEqual({ + walletActivity: { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + accounts: [ + { + address: ADDRESS_1.toLowerCase(), + enabled: true, + }, + { + address: ADDRESS_2.toLowerCase(), + enabled: false, + }, + ], }, - { - accounts: [ADDRESS_2], - type: KeyringTypes.hd, - metadata: { - id: 'srp-2', - name: 'SRP 2', - }, + marketing: { + inAppNotificationsEnabled: false, + pushNotificationsEnabled: true, }, - ], + perps: { ...DEFAULT_PERPS_PREFERENCES }, + socialAI: { ...DEFAULT_SOCIAL_AI_PREFERENCES }, + }); + expect(mockEnablePushNotifications).toHaveBeenCalledWith([ + ADDRESS_1.toLowerCase(), + ]); }); - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, - }); + it('enables all wallet-activity accounts when Trigger API has no enabled accounts for first-time setup', async () => { + const { + messenger, + mockEnablePushNotifications, + mockUpdateNotifications, + mockKeyringControllerGetState, + } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); - await controller.createOnChainTriggers(); + mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + accounts: [ADDRESS_1, ADDRESS_2], + type: KeyringTypes.hd, + metadata: { id: 'srp-1', name: 'SRP 1' }, + }, + ], + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig({ + status: 200, + body: [ + { address: ADDRESS_1.toLowerCase(), enabled: false }, + { address: ADDRESS_2.toLowerCase(), enabled: false }, + ], + }); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(true); - expect(controller.state.subscriptionAccountsSeen).toStrictEqual([ - ADDRESS_1, - ADDRESS_2, - ]); - }); + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); - it('deduplicates and filters non-Ethereum accounts when creating triggers', async () => { - const { - messenger, - mockGetConfig, - mockUpdateNotifications, - mockKeyringControllerGetState, - } = arrangeMocks({ - // Mock no existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [], - }), - }); + await controller.createOnChainTriggers(); - mockKeyringControllerGetState.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - accounts: [ADDRESS_1, ADDRESS_1.toLowerCase(), 'NotAnAddress'], - type: KeyringTypes.hd, - metadata: { - id: 'srp-1', - name: 'SRP 1', - }, - }, - { - accounts: [ - ADDRESS_2, - '7xKXtg2CW6y7J2wMmkf8VbM8dYb6u3H3V8bLxT64d4oR', - ], - type: KeyringTypes.hd, - metadata: { - id: 'srp-2', - name: 'SRP 2', - }, - }, - ], + expect(mockTriggerQuery.isDone()).toBe(true); + const [writtenPrefs] = mockUpdateNotifications.mock.calls[0]; + expect(writtenPrefs.walletActivity.accounts).toStrictEqual([ + { address: ADDRESS_1.toLowerCase(), enabled: true }, + { address: ADDRESS_2.toLowerCase(), enabled: true }, + ]); + expect(mockEnablePushNotifications).toHaveBeenCalledWith([ + ADDRESS_1.toLowerCase(), + ADDRESS_2.toLowerCase(), + ]); }); - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, + it('defaults marketing notifications to disabled when no consent is supplied', async () => { + const { messenger, mockUpdateNotifications } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); + mockGetOnChainNotificationsConfig(); + + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); + + await controller.createOnChainTriggers(); + + const [writtenPrefs] = mockUpdateNotifications.mock.calls[0]; + expect(writtenPrefs.marketing).toStrictEqual({ + inAppNotificationsEnabled: false, + pushNotificationsEnabled: false, + }); }); - await controller.createOnChainTriggers(); + it('enables marketing in-app notifications when product announcements are enabled', async () => { + const { messenger, mockUpdateNotifications } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); + mockGetOnChainNotificationsConfig(); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(true); - expect(controller.state.subscriptionAccountsSeen).toStrictEqual([ - ADDRESS_1, - ADDRESS_2, - ]); - }); + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); - it('normalizes non-checksummed mixed-case addresses before filtering', async () => { - const { - messenger, - mockGetConfig, - mockUpdateNotifications, - mockKeyringControllerGetState, - } = arrangeMocks({ - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [], - }), + await controller.createOnChainTriggers({ + productAnnouncementEnabled: true, + }); + + const [writtenPrefs] = mockUpdateNotifications.mock.calls[0]; + expect(writtenPrefs.marketing).toStrictEqual({ + inAppNotificationsEnabled: true, + pushNotificationsEnabled: false, + }); }); - const nonChecksummedMixedCaseAddress = - '0xd8Da6bf26964af9d7eeD9e03E53415D37aa96045'; + it('tracks accounts from all keyrings when creating triggers', async () => { + const { + messenger, + mockGetConfig, + mockUpdateNotifications, + mockKeyringControllerGetState, + } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); - mockKeyringControllerGetState.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - accounts: [nonChecksummedMixedCaseAddress], - type: KeyringTypes.hd, - metadata: { - id: 'srp-1', - name: 'SRP 1', + mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + accounts: [ADDRESS_1], + type: KeyringTypes.hd, + metadata: { id: 'srp-1', name: 'SRP 1' }, }, - }, - ], - }); + { + accounts: [ADDRESS_2], + type: KeyringTypes.hd, + metadata: { id: 'srp-2', name: 'SRP 2' }, + }, + ], + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig({ + status: 200, + body: [ + { address: ADDRESS_1.toLowerCase(), enabled: true }, + { address: ADDRESS_2.toLowerCase(), enabled: true }, + ], + }); - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); + + await controller.createOnChainTriggers(); + + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockTriggerQuery.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalled(); + expect(controller.state.subscriptionAccountsSeen).toStrictEqual([ + ADDRESS_1, + ADDRESS_2, + ]); }); - await controller.createOnChainTriggers(); + it('deduplicates and filters non-Ethereum accounts when creating triggers', async () => { + const { + messenger, + mockGetConfig, + mockUpdateNotifications, + mockKeyringControllerGetState, + } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(true); - expect(controller.state.subscriptionAccountsSeen).toStrictEqual([ - ADDRESS_1, - ]); - }); + mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + accounts: [ADDRESS_1, ADDRESS_1.toLowerCase(), 'NotAnAddress'], + type: KeyringTypes.hd, + metadata: { id: 'srp-1', name: 'SRP 1' }, + }, + { + accounts: [ + ADDRESS_2, + '7xKXtg2CW6y7J2wMmkf8VbM8dYb6u3H3V8bLxT64d4oR', + ], + type: KeyringTypes.hd, + metadata: { id: 'srp-2', name: 'SRP 2' }, + }, + ], + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig({ + status: 200, + body: [ + { address: ADDRESS_1.toLowerCase(), enabled: true }, + { address: ADDRESS_2.toLowerCase(), enabled: true }, + ], + }); - it('does not register notifications when notifications already exist and not resetting (however does update push registrations)', async () => { - const { - messenger, - mockEnablePushNotifications, - mockGetConfig, - mockUpdateNotifications, - } = arrangeMocks({ - // Mock existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [{ address: ADDRESS_1, enabled: true }], - }), - }); + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, + await controller.createOnChainTriggers(); + + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockTriggerQuery.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalled(); + expect(controller.state.subscriptionAccountsSeen).toStrictEqual([ + ADDRESS_1, + ADDRESS_2, + ]); }); - await controller.createOnChainTriggers(); + it('normalizes non-checksummed mixed-case addresses before filtering', async () => { + const { + messenger, + mockGetConfig, + mockUpdateNotifications, + mockKeyringControllerGetState, + } = arrangeMocks({ + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), + }); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(false); // we do not update notification subscriptions - expect(mockEnablePushNotifications).toHaveBeenCalled(); // but we do lazily update push subscriptions - }); + const nonChecksummedMixedCaseAddress = + '0xd8Da6bf26964af9d7eeD9e03E53415D37aa96045'; - it('creates new triggers when resetNotifications is true even if notifications exist', async () => { - const { - messenger, - mockEnablePushNotifications, - mockGetConfig, - mockUpdateNotifications, - } = arrangeMocks({ - // Mock existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [{ address: ADDRESS_1, enabled: true }], - }), - }); + mockKeyringControllerGetState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + accounts: [nonChecksummedMixedCaseAddress], + type: KeyringTypes.hd, + metadata: { id: 'srp-1', name: 'SRP 1' }, + }, + ], + }); + const mockTriggerQuery = mockGetOnChainNotificationsConfig({ + status: 200, + body: [{ address: ADDRESS_1.toLowerCase(), enabled: true }], + }); - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, - }); + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); - await controller.createOnChainTriggers({ resetNotifications: true }); + await controller.createOnChainTriggers(); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(true); - expect(mockEnablePushNotifications).toHaveBeenCalled(); + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockTriggerQuery.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalled(); + expect(controller.state.subscriptionAccountsSeen).toStrictEqual([ + ADDRESS_1, + ]); + }); }); - it('preserves user preferences when re-subscribing using enableMetamaskNotifications', async () => { - const { - messenger, - mockEnablePushNotifications, - mockGetConfig, - mockUpdateNotifications, - } = arrangeMocks({ - // Mock existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [{ address: ADDRESS_1, enabled: true }], - }), - }); + describe('when AUS preferences are fully initialized', () => { + it('does not register notifications when notifications already exist and not resetting (however does update push registrations)', async () => { + const { + messenger, + mockEnablePushNotifications, + mockGetConfig, + mockUpdateNotifications, + } = arrangeMocks({ + configurePrefs: (mock) => + mock.mockResolvedValueOnce( + prefsFromAddresses([{ address: ADDRESS_1, enabled: true }]), + ), + }); - // User has disabled feature announcements - const controller = new NotificationServicesController({ - messenger, - env: { featureAnnouncements: featureAnnouncementsEnv }, - state: { - isNotificationServicesEnabled: true, - isFeatureAnnouncementsEnabled: false, - }, + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + }); + + await controller.createOnChainTriggers(); + + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockUpdateNotifications).not.toHaveBeenCalled(); + expect(mockEnablePushNotifications).toHaveBeenCalled(); }); - await controller.enableMetamaskNotifications(); + it('preserves user preferences when re-subscribing using enableMetamaskNotifications', async () => { + const { + messenger, + mockEnablePushNotifications, + mockGetConfig, + mockUpdateNotifications, + } = arrangeMocks({ + configurePrefs: (mock) => + mock.mockResolvedValueOnce( + prefsFromAddresses([{ address: ADDRESS_1, enabled: true }]), + ), + }); - // Feature announcements should remain disabled - expect(controller.state.isFeatureAnnouncementsEnabled).toBe(false); - expect(controller.state.isNotificationServicesEnabled).toBe(true); - expect(mockGetConfig.isDone()).toBe(true); - expect(mockUpdateNotifications.isDone()).toBe(false); - expect(mockEnablePushNotifications).toHaveBeenCalled(); + const controller = new NotificationServicesController({ + messenger, + env: { featureAnnouncements: featureAnnouncementsEnv }, + state: { + isNotificationServicesEnabled: true, + isFeatureAnnouncementsEnabled: false, + }, + }); + + await controller.enableMetamaskNotifications(); + + expect(controller.state.isFeatureAnnouncementsEnabled).toBe(false); + expect(controller.state.isNotificationServicesEnabled).toBe(true); + expect(mockGetConfig).toHaveBeenCalled(); + expect(mockUpdateNotifications).not.toHaveBeenCalled(); + expect(mockEnablePushNotifications).toHaveBeenCalled(); + }); }); it('throws if not given a valid auth & bearer token', async () => { @@ -765,10 +894,11 @@ describe('NotificationServicesController', () => { describe('disableAccounts', () => { const arrangeMocks = (): ReturnType & { - mockUpdateNotifications: nock.Scope; + mockUpdateNotifications: jest.Mock; } => { const messengerMocks = mockNotificationMessenger(); - const mockUpdateNotifications = mockUpdateOnChainNotifications(); + const mockUpdateNotifications = + messengerMocks.mockPutNotificationPreferences; return { ...messengerMocks, mockUpdateNotifications }; }; @@ -785,7 +915,7 @@ describe('NotificationServicesController', () => { await controller.disableAccounts([ADDRESS_1]); - expect(mockUpdateNotifications.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalled(); expect(mockDeletePushNotificationLinks).toHaveBeenCalledWith([ADDRESS_1]); }); @@ -812,10 +942,11 @@ describe('NotificationServicesController', () => { describe('enableAccounts', () => { const arrangeMocks = (): ReturnType & { - mockUpdateNotifications: nock.Scope; + mockUpdateNotifications: jest.Mock; } => { const messengerMocks = mockNotificationMessenger(); - const mockUpdateNotifications = mockUpdateOnChainNotifications(); + const mockUpdateNotifications = + messengerMocks.mockPutNotificationPreferences; return { ...messengerMocks, mockUpdateNotifications }; }; @@ -832,7 +963,7 @@ describe('NotificationServicesController', () => { await controller.enableAccounts([ADDRESS_1]); - expect(mockUpdateNotifications.isDone()).toBe(true); + expect(mockUpdateNotifications).toHaveBeenCalled(); expect(mockAddPushNotificationLinks).toHaveBeenCalledWith([ADDRESS_1]); }); @@ -859,7 +990,6 @@ describe('NotificationServicesController', () => { describe('fetchAndUpdateMetamaskNotifications', () => { const arrangeMocks = (): ReturnType & { - mockNotificationConfigAPI: nock.Scope; mockFeatureAnnouncementAPIResult: ReturnType< typeof createMockFeatureAnnouncementAPIResult >; @@ -879,8 +1009,6 @@ describe('NotificationServicesController', () => { body: mockFeatureAnnouncementAPIResult, }); - const mockNotificationConfigAPI = mockGetOnChainNotificationsConfig(); - const mockOnChainNotificationsAPIResult = [ createMockNotificationEthSent(), ]; @@ -891,7 +1019,6 @@ describe('NotificationServicesController', () => { return { ...messengerMocks, - mockNotificationConfigAPI, mockFeatureAnnouncementAPIResult, mockFeatureAnnouncementsAPI, mockOnChainNotificationsAPIResult, @@ -917,10 +1044,6 @@ describe('NotificationServicesController', () => { return controller; }; - beforeEach(() => { - clearAPICache(); - }); - it('processes and shows all notifications (announcements, wallet, and snap notifications)', async () => { const { messenger } = arrangeMocks(); const controller = arrangeController(messenger, { @@ -1250,15 +1373,16 @@ describe('NotificationServicesController', () => { describe('enableMetamaskNotifications', () => { const arrangeMocks = (overrides?: { - mockGetConfig: () => nock.Scope; + configurePrefs?: (mock: jest.Mock) => void; }): ReturnType & { - mockGetConfig: nock.Scope; - mockUpdateNotifications: nock.Scope; + mockGetConfig: jest.Mock; + mockUpdateNotifications: jest.Mock; } => { const messengerMocks = mockNotificationMessenger(); - const mockGetConfig = - overrides?.mockGetConfig() ?? mockGetOnChainNotificationsConfig(); - const mockUpdateNotifications = mockUpdateOnChainNotifications(); + const mockGetConfig = messengerMocks.mockGetNotificationPreferences; + const mockUpdateNotifications = + messengerMocks.mockPutNotificationPreferences; + overrides?.configurePrefs?.(mockGetConfig); messengerMocks.mockKeyringControllerGetState.mockReturnValue({ isUnlocked: true, @@ -1274,13 +1398,13 @@ describe('NotificationServicesController', () => { ], }); - return { ...messengerMocks, mockGetConfig, mockUpdateNotifications }; + return { + ...messengerMocks, + mockGetConfig, + mockUpdateNotifications, + }; }; - beforeEach(() => { - clearAPICache(); - }); - it('should sign a user in if not already signed in', async () => { const mocks = arrangeMocks(); mocks.mockIsSignedIn.mockReturnValue(false); // mock that auth is not enabled @@ -1298,9 +1422,8 @@ describe('NotificationServicesController', () => { it('create new notifications when switched on and no existing notifications', async () => { const mocks = arrangeMocks({ - // Mock no existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ status: 200, body: [] }), + // No AUS preferences yet — fresh initialization. + configurePrefs: (mock) => mock.mockResolvedValueOnce(null), }); const controller = new NotificationServicesController({ @@ -1320,18 +1443,17 @@ describe('NotificationServicesController', () => { expect(controller.state.isNotificationServicesEnabled).toBe(true); // Act - services called - expect(mocks.mockGetConfig.isDone()).toBe(true); - expect(mocks.mockUpdateNotifications.isDone()).toBe(true); + expect(mocks.mockGetConfig).toHaveBeenCalled(); + expect(mocks.mockUpdateNotifications).toHaveBeenCalled(); }); it('should not create new notification subscriptions when enabling an account that already has notifications', async () => { const mocks = arrangeMocks({ - // Mock existing notifications - mockGetConfig: () => - mockGetOnChainNotificationsConfig({ - status: 200, - body: [{ address: ADDRESS_1, enabled: true }], - }), + // Mock fully-initialized existing notifications + configurePrefs: (mock) => + mock.mockResolvedValueOnce( + prefsFromAddresses([{ address: ADDRESS_1, enabled: true }]), + ), }); const controller = new NotificationServicesController({ @@ -1341,8 +1463,8 @@ describe('NotificationServicesController', () => { await controller.enableMetamaskNotifications(); - expect(mocks.mockGetConfig.isDone()).toBe(true); - expect(mocks.mockUpdateNotifications.isDone()).toBe(false); + expect(mocks.mockGetConfig).toHaveBeenCalled(); + expect(mocks.mockUpdateNotifications).not.toHaveBeenCalled(); }); }); @@ -1421,16 +1543,16 @@ describe('NotificationServicesController', () => { describe('enablePushNotifications', () => { const arrangeMocks = (): ReturnType & { - mockGetConfig: nock.Scope; + mockGetConfig: jest.Mock; } => { const messengerMocks = mockNotificationMessenger(); - const mockGetConfig = mockGetOnChainNotificationsConfig({ - status: 200, - body: [ + const mockGetConfig = messengerMocks.mockGetNotificationPreferences; + mockGetConfig.mockResolvedValueOnce( + prefsFromAddresses([ { address: ADDRESS_1, enabled: true }, { address: ADDRESS_2, enabled: true }, - ], - }); + ]), + ); return { ...messengerMocks, mockGetConfig }; }; @@ -1447,30 +1569,30 @@ describe('NotificationServicesController', () => { await controller.enablePushNotifications(); // Assert - expect(mockGetConfig.isDone()).toBe(true); + expect(mockGetConfig).toHaveBeenCalled(); + // Addresses are stored lower-cased in AUS preferences. expect(mockEnablePushNotifications).toHaveBeenCalledWith([ - ADDRESS_1, - ADDRESS_2, + ADDRESS_1.toLowerCase(), + ADDRESS_2.toLowerCase(), ]); }); it('handles errors gracefully when fetching notification config fails', async () => { - const { messenger, mockEnablePushNotifications } = - mockNotificationMessenger(); - - // Mock API failure - mockGetOnChainNotificationsConfig({ status: 500 }); + const mocks = mockNotificationMessenger(); + mocks.mockGetNotificationPreferences.mockRejectedValueOnce( + new Error('mock api failure'), + ); mockErrorLog(); const controller = new NotificationServicesController({ - messenger, + messenger: mocks.messenger, env: { featureAnnouncements: featureAnnouncementsEnv }, state: { isNotificationServicesEnabled: true }, }); // Should not throw error await controller.enablePushNotifications(); - expect(mockEnablePushNotifications).not.toHaveBeenCalled(); + expect(mocks.mockEnablePushNotifications).not.toHaveBeenCalled(); }); }); @@ -1724,6 +1846,8 @@ function mockNotificationMessenger(): { mockEnablePushNotifications: jest.Mock; mockSubscribeToPushNotifications: jest.Mock; mockKeyringControllerGetState: jest.Mock; + mockGetNotificationPreferences: jest.Mock; + mockPutNotificationPreferences: jest.Mock; } { const globalMessenger = getRootMessenger(); @@ -1749,6 +1873,8 @@ function mockNotificationMessenger(): { 'NotificationServicesPushController:deletePushNotificationLinks', 'NotificationServicesPushController:enablePushNotifications', 'NotificationServicesPushController:subscribeToPushNotifications', + 'AuthenticatedUserStorageService:getNotificationPreferences', + 'AuthenticatedUserStorageService:putNotificationPreferences', ], events: [ 'KeyringController:stateChange', @@ -1804,6 +1930,16 @@ function mockNotificationMessenger(): { ], } as MockVar); + const mockGetNotificationPreferences = + typedMockAction().mockResolvedValue( + prefsFromAddresses([{ address: '0xTestAddress', enabled: true }]), + ); + + const mockPutNotificationPreferences = + typedMockAction().mockResolvedValue( + undefined, + ); + jest.spyOn(messenger, 'call').mockImplementation((...args) => { const [actionType] = args; @@ -1863,6 +1999,20 @@ function mockNotificationMessenger(): { return mockSubscribeToPushNotifications(); } + if ( + actionType === + 'AuthenticatedUserStorageService:getNotificationPreferences' + ) { + return mockGetNotificationPreferences(); + } + + if ( + actionType === + 'AuthenticatedUserStorageService:putNotificationPreferences' + ) { + return mockPutNotificationPreferences(params[0], params[1]); + } + throw new Error( `MOCK_FAIL - unsupported messenger call: ${actionType as string}`, ); @@ -1880,6 +2030,8 @@ function mockNotificationMessenger(): { mockEnablePushNotifications, mockSubscribeToPushNotifications, mockKeyringControllerGetState, + mockGetNotificationPreferences, + mockPutNotificationPreferences, }; } diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index 8a23e1b1fb..5ddc45c43e 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -1,3 +1,11 @@ +import type { + AuthenticatedUserStorageServiceGetNotificationPreferencesAction, + AuthenticatedUserStorageServicePutNotificationPreferencesAction, + NotificationPreferences, + PerpsPreference, + SocialAIPreference, + WalletActivityAccount, +} from '@metamask/authenticated-user-storage'; import type { ControllerGetStateAction, ControllerStateChangeEvent, @@ -17,6 +25,7 @@ import type { } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { AuthenticationController } from '@metamask/profile-sync-controller'; +import type { Hex } from '@metamask/utils'; import { assert } from '@metamask/utils'; import { debounce } from 'lodash'; import log from 'loglevel'; @@ -38,7 +47,6 @@ import { getAPINotifications, getNotificationsApiConfigCached, markNotificationsAsRead, - updateOnChainNotifications, } from './services/api-notifications'; import { getFeatureAnnouncementNotifications } from './services/feature-announcements'; import { createPerpOrderNotification } from './services/perp-notifications'; @@ -183,10 +191,128 @@ export const defaultState: NotificationServicesControllerState = { isCheckingAccountsPresence: false, }; +export type NotificationServicesControllerEnableNotificationsOptions = { + /** + * Whether the user has consented to marketing notifications. Used only when + * notification preferences are being initialized for the first time to seed + * marketing push notifications. + */ + hasMarketingConsent?: boolean; + /** + * Whether product announcements are enabled. Used only when notification + * preferences are being initialized for the first time to seed marketing + * in-app notifications. + */ + productAnnouncementEnabled?: boolean; +}; + const locallyPersistedNotificationTypes = new Set([ TRIGGER_TYPES.SNAP, ]); +/** + * Hardcoded default Perps notification preferences. Applied when notification + * preferences are initialized for the first time. + */ +export const DEFAULT_PERPS_PREFERENCES: PerpsPreference = { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, +}; + +/** + * Hardcoded default Social AI notification preferences. Applied when + * notification preferences are initialized for the first time. + */ +export const DEFAULT_SOCIAL_AI_PREFERENCES: Required = { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + txAmountLimit: 500, + mutedTraderProfileIds: [], +}; + +/** + * Builds wallet-activity preferences from the keyring's current accounts. + * + * @param accounts - The keyring accounts to build wallet-activity entries for. + * @returns An array of wallet-activity account entries (lower-cased addresses). + */ +const buildWalletActivityAccounts = ( + accounts: { address: string; enabled: boolean }[], +): WalletActivityAccount[] => + accounts.map(({ address, enabled }) => { + const lowercased = address.toLowerCase(); + return { + address: lowercased as Hex, + enabled, + }; + }); + +/** + * Builds wallet-activity initialization from the Trigger API. If Trigger has no + * enabled entries for the current keyring accounts, this is a first-time + * notification setup and all current accounts should start enabled. + * + * @param bearerToken - JWT used to query Trigger API. + * @param accounts - The keyring accounts to initialize. + * @param env - The environment to use for the Trigger API call. + * @returns Wallet-activity account initialization entries. + */ +const buildWalletActivityAccountsFromTriggerConfig = async ( + bearerToken: string, + accounts: string[], + env: ENV, +): Promise<{ address: string; enabled: boolean }[]> => { + const triggerConfig = await getNotificationsApiConfigCached( + bearerToken, + accounts, + env, + ); + const triggerConfigByAddress = new Map( + triggerConfig.map(({ address, enabled }) => [ + address.toLowerCase(), + enabled, + ]), + ); + const hasEnabledTriggerAccount = accounts.some( + (address) => triggerConfigByAddress.get(address.toLowerCase()) === true, + ); + + return accounts.map((address) => ({ + address, + enabled: hasEnabledTriggerAccount + ? (triggerConfigByAddress.get(address.toLowerCase()) ?? false) + : true, + })); +}; + +/** + * Builds a fresh `NotificationPreferences` blob using hardcoded defaults for + * Perps and Social AI, the supplied wallet-activity accounts and the user's + * marketing/product-announcement flags. + * + * @param walletActivityAccounts - The wallet-activity account config to initialize. + * @param hasMarketingConsent - Whether marketing push notifications should be enabled. + * @param productAnnouncementEnabled - Whether marketing in-app notifications should be enabled. + * @returns A complete `NotificationPreferences` object. + */ +const buildFreshPreferences = ( + walletActivityAccounts: { address: string; enabled: boolean }[], + hasMarketingConsent: boolean, + productAnnouncementEnabled: boolean, +): NotificationPreferences => ({ + walletActivity: { + inAppNotificationsEnabled: true, + pushNotificationsEnabled: true, + accounts: buildWalletActivityAccounts(walletActivityAccounts), + }, + marketing: { + inAppNotificationsEnabled: productAnnouncementEnabled, + pushNotificationsEnabled: hasMarketingConsent, + }, + perps: { ...DEFAULT_PERPS_PREFERENCES }, + socialAI: { ...DEFAULT_SOCIAL_AI_PREFERENCES }, +}); + const MESSENGER_EXPOSED_METHODS = [ 'init', 'enablePushNotifications', @@ -226,6 +352,9 @@ type AllowedActions = | AuthenticationController.AuthenticationControllerGetBearerTokenAction | AuthenticationController.AuthenticationControllerIsSignedInAction | AuthenticationController.AuthenticationControllerPerformSignInAction + // Authenticated User Storage Requests + | AuthenticatedUserStorageServiceGetNotificationPreferencesAction + | AuthenticatedUserStorageServicePutNotificationPreferencesAction // Push Notifications Controller Requests | NotificationServicesPushControllerMethodActions; @@ -651,6 +780,72 @@ export class NotificationServicesController extends BaseController< return { bearerToken }; } + /** + * Updates the `walletActivity.accounts` entries in the user's + * notification-preferences blob in {@link AuthenticatedUserStorageService}. + * + * `putNotificationPreferences` replaces the entire blob, so we read the + * current preferences, merge the supplied updates into + * `walletActivity.accounts`, and write the result back. This helper is only + * meant to be used for incremental updates (enable/disable individual + * accounts) after the preferences blob has already been initialized via + * {@link createOnChainTriggers}; callers should not rely on it to perform + * first-time initialization. + * + * @param updates - Addresses to register, each with the desired `enabled` flag + */ + async #registerWalletActivityAddresses( + updates: { address: string; enabled: boolean }[], + ): Promise { + if (updates.length === 0) { + return; + } + + const currentPreferences = await this.messenger + .call('AuthenticatedUserStorageService:getNotificationPreferences') + .catch((error) => { + log.error( + 'Failed to get notification preferences. Re-initializing them instead.', + error, + ); + // TODO: return null once validation error captured + // return null; + throw error; + }); + + if (!currentPreferences) { + log.warn( + 'Preferences blob not yet initialized; run `createOnChainTriggers` first.', + ); + return; + } + + const accountsByAddress = new Map( + currentPreferences.walletActivity.accounts.map((account) => [ + account.address.toLowerCase(), + { ...account, address: account.address.toLowerCase() as Hex }, + ]), + ); + for (const update of updates) { + const address = update.address.toLowerCase() as Hex; + accountsByAddress.set(address, { address, enabled: update.enabled }); + } + + const nextPreferences: NotificationPreferences = { + ...currentPreferences, + walletActivity: { + ...currentPreferences.walletActivity, + accounts: [...accountsByAddress.values()], + }, + }; + + await this.messenger.call( + 'AuthenticatedUserStorageService:putNotificationPreferences', + nextPreferences, + this.#featureAnnouncementEnv.platform, + ); + } + /** * Sets the state of notification creation process. * @@ -735,18 +930,14 @@ export class NotificationServicesController extends BaseController< */ public async enablePushNotifications(): Promise { try { - const { bearerToken } = await this.#getBearerToken(); - const { accounts } = this.#accounts.listAccounts(); - const addressesWithNotifications = await getNotificationsApiConfigCached( - bearerToken, - accounts, - this.#env, + const preferences = await this.messenger.call( + 'AuthenticatedUserStorageService:getNotificationPreferences', ); - const addresses = addressesWithNotifications - .filter((addressConfig) => Boolean(addressConfig.enabled)) - .map((addressConfig) => addressConfig.address); - if (addresses.length > 0) { - await this.#pushNotifications.enablePushNotifications(addresses); + const enabledAddresses = (preferences?.walletActivity.accounts ?? []) + .filter((account) => account.enabled) + .map((account) => account.address); + if (enabledAddresses.length > 0) { + await this.#pushNotifications.enablePushNotifications(enabledAddresses); } } catch { // Do nothing, failing silently. @@ -766,18 +957,20 @@ export class NotificationServicesController extends BaseController< try { this.#setIsCheckingAccountsPresence(true); - // Retrieve user storage - const { bearerToken } = await this.#getBearerToken(); - const addressesWithNotifications = await getNotificationsApiConfigCached( - bearerToken, - accounts, - this.#env, + const preferences = await this.messenger.call( + 'AuthenticatedUserStorageService:getNotificationPreferences', + ); + const enabledByAddress = new Map( + (preferences?.walletActivity.accounts ?? []).map((account) => [ + account.address.toLowerCase(), + account.enabled, + ]), ); const result: Record = {}; - addressesWithNotifications.forEach((a) => { - result[a.address] = a.enabled; - }); + for (const address of accounts) { + result[address] = enabledByAddress.get(address.toLowerCase()) ?? false; + } return result; } catch (error) { log.error('Failed to check accounts presence', error); @@ -814,15 +1007,21 @@ export class NotificationServicesController extends BaseController< * * **Action** - Used during Sign In / Enabling of notifications. * + * Notification preferences are initialized only when + * {@link AuthenticatedUserStorageService} has no stored preferences yet. + * Existing preferences are left as-is. + * * @param opts - optional options to mutate this functionality - * @param opts.resetNotifications - this will not use the users stored preferences, and instead re-create notification triggers - * It will help in case uses get into a corrupted state or wants to wipe their notifications. + * @param opts.hasMarketingConsent - The user's marketing-consent flag. + * Used only during initialization to seed marketing push notifications. + * @param opts.productAnnouncementEnabled - The user's product-announcement flag. + * Used only during initialization to seed marketing in-app notifications. * @returns The updated or newly created user storage. * @throws {Error} Throws an error if unauthenticated or from other operations. */ - public async createOnChainTriggers(opts?: { - resetNotifications?: boolean; - }): Promise { + public async createOnChainTriggers( + opts?: NotificationServicesControllerEnableNotificationsOptions, + ): Promise { try { this.#setIsUpdatingMetamaskNotifications(true); @@ -830,30 +1029,48 @@ export class NotificationServicesController extends BaseController< const { accounts } = this.#accounts.listAccounts(); - // 1. See if has enabled notifications before - const addressesWithNotifications = await getNotificationsApiConfigCached( - bearerToken, - accounts, - this.#env, + // 1. Read existing AUS notification preferences and initialize only if absent. + const preferences = await this.messenger.call( + 'AuthenticatedUserStorageService:getNotificationPreferences', + ); + + const hasMarketingConsent = Boolean(opts?.hasMarketingConsent); + const productAnnouncementEnabled = Boolean( + opts?.productAnnouncementEnabled, ); + let nextPreferences: NotificationPreferences | undefined; + + if (preferences === null) { + const walletActivityAccounts = + await buildWalletActivityAccountsFromTriggerConfig( + bearerToken, + accounts, + this.#env, + ); - // Notifications API can return array with addresses set to false - // So assert that at least one address is enabled - let accountsWithNotifications = addressesWithNotifications - .filter((addressConfig) => Boolean(addressConfig.enabled)) - .map((addressConfig) => addressConfig.address); - - // 2. Enable Notifications (if no accounts subscribed or we are resetting) - if (accountsWithNotifications.length === 0 || opts?.resetNotifications) { - await updateOnChainNotifications( - bearerToken, - accounts.map((address) => ({ address, enabled: true })), - this.#env, + nextPreferences = buildFreshPreferences( + walletActivityAccounts, + hasMarketingConsent, + productAnnouncementEnabled, ); - accountsWithNotifications = accounts; } - // 3. Lazily enable push notifications (FCM may take some time, so keeps UI unblocked) + if (nextPreferences) { + await this.messenger.call( + 'AuthenticatedUserStorageService:putNotificationPreferences', + nextPreferences, + this.#featureAnnouncementEnv.platform, + ); + } + + const effectivePreferences = nextPreferences ?? preferences; + const accountsWithNotifications = ( + effectivePreferences?.walletActivity.accounts ?? [] + ) + .filter((account) => account.enabled) + .map((account) => account.address); + + // 2. Lazily enable push notifications (FCM may take some time, so keeps UI unblocked) this.#pushNotifications .enablePushNotifications(accountsWithNotifications) .catch(() => { @@ -885,13 +1102,16 @@ export class NotificationServicesController extends BaseController< * Enables all MetaMask notifications for the user. * This is identical flow when initializing notifications for the first time. * + * @param opts - Optional settings for first-time AUS notification preferences initialization. * @throws {Error} If there is an error during the process of enabling notifications. */ - public async enableMetamaskNotifications(): Promise { + public async enableMetamaskNotifications( + opts?: NotificationServicesControllerEnableNotificationsOptions, + ): Promise { try { this.#setIsUpdatingMetamaskNotifications(true); await this.#enableAuth(); - await this.createOnChainTriggers(); + await this.createOnChainTriggers(opts); } catch (error) { log.error('Unable to enable notifications', error); throw new Error('Unable to enable notifications'); @@ -950,14 +1170,11 @@ export class NotificationServicesController extends BaseController< public async disableAccounts(accounts: string[]): Promise { try { this.#updateUpdatingAccountsState(accounts); - // Get and Validate BearerToken and User Storage Key - const { bearerToken } = await this.#getBearerToken(); + // Sign-in gate. + await this.#getBearerToken(); - // Delete these UUIDs (Mutates User Storage) - await updateOnChainNotifications( - bearerToken, + await this.#registerWalletActivityAddresses( accounts.map((address) => ({ address, enabled: false })), - this.#env, ); await this.#pushNotifications.deletePushNotificationLinks(accounts); @@ -987,11 +1204,10 @@ export class NotificationServicesController extends BaseController< try { this.#updateUpdatingAccountsState(accounts); - const { bearerToken } = await this.#getBearerToken(); - await updateOnChainNotifications( - bearerToken, + // Sign-in gate. + await this.#getBearerToken(); + await this.#registerWalletActivityAddresses( accounts.map((address) => ({ address, enabled: true })), - this.#env, ); await this.#pushNotifications.addPushNotificationLinks(accounts); @@ -1037,16 +1253,14 @@ export class NotificationServicesController extends BaseController< if (isGlobalNotifsEnabled) { try { const { bearerToken } = await this.#getBearerToken(); - const { accounts } = this.#accounts.listAccounts(); + const preferences = await this.messenger.call( + 'AuthenticatedUserStorageService:getNotificationPreferences', + ); const addressesWithNotifications = ( - await getNotificationsApiConfigCached( - bearerToken, - accounts, - this.#env, - ) + preferences?.walletActivity.accounts ?? [] ) - .filter((addressConfig) => Boolean(addressConfig.enabled)) - .map((addressConfig) => addressConfig.address); + .filter((account) => account.enabled) + .map((account) => account.address); const notifications = await getAPINotifications( bearerToken, addressesWithNotifications, diff --git a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts index 44ac409c51..449383d192 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts @@ -1,7 +1,6 @@ import nock from 'nock'; import { - getMockUpdateOnChainNotifications, getMockOnChainNotificationsConfig, getMockFeatureAnnouncementResponse, getMockListNotificationsResponse, @@ -27,19 +26,6 @@ export const mockFetchFeatureAnnouncementNotifications = ( return mockEndpoint; }; -export const mockUpdateOnChainNotifications = ( - mockReply?: MockReply, -): nock.Scope => { - const mockResponse = getMockUpdateOnChainNotifications(); - const reply = mockReply ?? { status: 204 }; - - const mockEndpoint = nock(mockResponse.url) - .post('') - .reply(reply.status, reply.body); - - return mockEndpoint; -}; - export const mockGetOnChainNotificationsConfig = ( mockReply?: MockReply, ): nock.Scope => { diff --git a/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts b/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts index 1eb91a1bc4..f6bd477301 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts @@ -1,7 +1,6 @@ import { NOTIFICATION_API_LIST_ENDPOINT, NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, - TRIGGER_API_NOTIFICATIONS_ENDPOINT, TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT, } from '../services/api-notifications'; import { FEATURE_ANNOUNCEMENT_API } from '../services/feature-announcements'; @@ -18,8 +17,7 @@ type MockResponse = { export const CONTENTFUL_RESPONSE = createMockFeatureAnnouncementAPIResult(); // Using `satisfies` to preserve narrow return types while ensuring type safety; explicit return types would widen to MockResponse -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -export const getMockFeatureAnnouncementResponse = () => { +export const getMockFeatureAnnouncementResponse = (): MockResponse => { return { url: FEATURE_ANNOUNCEMENT_API, requestMethod: 'GET', @@ -27,29 +25,19 @@ export const getMockFeatureAnnouncementResponse = () => { } satisfies MockResponse; }; -export const getMockUpdateOnChainNotifications = () => { - return { - url: TRIGGER_API_NOTIFICATIONS_ENDPOINT(), - requestMethod: 'POST', - response: null, - } satisfies MockResponse; -}; - -export const getMockOnChainNotificationsConfig = () => { +export const getMockOnChainNotificationsConfig = (): MockResponse => { return { url: TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT(), requestMethod: 'POST', response: [{ address: '0xTestAddress', enabled: true }], } satisfies MockResponse; }; -/* eslint-enable @typescript-eslint/explicit-function-return-type */ export const MOCK_RAW_ON_CHAIN_NOTIFICATIONS = createMockRawOnChainNotifications(); // Using `satisfies` to preserve narrow return types while ensuring type safety; explicit return types would widen to MockResponse -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -export const getMockListNotificationsResponse = () => { +export const getMockListNotificationsResponse = (): MockResponse => { return { url: NOTIFICATION_API_LIST_ENDPOINT(), requestMethod: 'POST', @@ -57,7 +45,7 @@ export const getMockListNotificationsResponse = () => { } satisfies MockResponse; }; -export const getMockMarkNotificationsAsReadResponse = () => { +export const getMockMarkNotificationsAsReadResponse = (): MockResponse => { return { url: NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT(), requestMethod: 'POST', @@ -65,11 +53,10 @@ export const getMockMarkNotificationsAsReadResponse = () => { } satisfies MockResponse; }; -export const getMockCreatePerpOrderNotification = () => { +export const getMockCreatePerpOrderNotification = (): MockResponse => { return { url: PERPS_API_CREATE_ORDERS, requestMethod: 'POST', response: null, } satisfies MockResponse; }; -/* eslint-enable @typescript-eslint/explicit-function-return-type */ diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts index c6ff388642..0fb4c9d566 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts @@ -1,6 +1,5 @@ import { mockGetOnChainNotificationsConfig, - mockUpdateOnChainNotifications, mockGetAPINotifications, mockMarkNotificationsAsRead, } from '../__fixtures__/mockServices'; @@ -57,62 +56,6 @@ describe('On Chain Notifications - getAPINotificationsConfig()', () => { }); }); -describe('On Chain Notifications - updateOnChainNotifications()', () => { - const mockAddressesWithStatus = [ - { address: '0x123', enabled: true }, - { address: '0x456', enabled: false }, - { address: '0x789', enabled: true }, - ]; - - it('should successfully update notification settings', async () => { - const mockEndpoint = mockUpdateOnChainNotifications(); - - await OnChainNotifications.updateOnChainNotifications( - MOCK_BEARER_TOKEN, - mockAddressesWithStatus, - ); - - expect(mockEndpoint.isDone()).toBe(true); - }); - - it('should bail early if given empty list of addresses', async () => { - const mockEndpoint = mockUpdateOnChainNotifications(); - - await OnChainNotifications.updateOnChainNotifications( - MOCK_BEARER_TOKEN, - [], - ); - - expect(mockEndpoint.isDone()).toBe(false); // bailed before API was called - }); - - it('should handle endpoint failure gracefully', async () => { - const mockBadEndpoint = mockUpdateOnChainNotifications({ - status: 500, - body: { error: 'mock api failure' }, - }); - - // Should not throw error, should handle gracefully - await OnChainNotifications.updateOnChainNotifications( - MOCK_BEARER_TOKEN, - mockAddressesWithStatus, - ); - - expect(mockBadEndpoint.isDone()).toBe(true); - }); - - it('should send addresses with enabled status in request body', async () => { - const mockEndpoint = mockUpdateOnChainNotifications(); - - await OnChainNotifications.updateOnChainNotifications( - MOCK_BEARER_TOKEN, - mockAddressesWithStatus, - ); - - expect(mockEndpoint.isDone()).toBe(true); - }); -}); - describe('On Chain Notifications - getAPINotifications()', () => { it('should return a list of notifications', async () => { const mockEndpoint = mockGetAPINotifications(); diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts index f4ab64d8e0..14276ebb99 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts @@ -9,13 +9,6 @@ import type { import { makeApiCall } from '../utils/utils'; import { notificationsConfigCache } from './notification-config-cache'; -export type NotificationTrigger = { - id: string; - chainId: string; - kind: string; - address: string; -}; - export type ENV = 'prd' | 'uat' | 'dev'; const TRIGGER_API_ENV = { @@ -41,11 +34,7 @@ export const TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT = ( env: ENV = 'prd', ): string => `${TRIGGER_API(env)}/api/v2/notifications/query`; -// Used to create/update account notifications for each account provided -export const TRIGGER_API_NOTIFICATIONS_ENDPOINT = (env: ENV = 'prd'): string => - `${TRIGGER_API(env)}/api/v2/notifications`; - -// Lists notifications for each account provided +// Lists notifications for each address provided export const NOTIFICATION_API_LIST_ENDPOINT = (env: ENV = 'prd'): string => `${NOTIFICATION_API(env)}/api/v3/notifications`; @@ -101,40 +90,6 @@ export async function getNotificationsApiConfigCached( return result; } -/** - * updates notifications for a given addresses - * - * @param bearerToken - jwt - * @param addresses - list of addresses to check - * @param env - the environment to use for the API call - * @returns void - */ -export async function updateOnChainNotifications( - bearerToken: string, - addresses: { address: string; enabled: boolean }[], - env: ENV = 'prd', -): Promise { - if (addresses.length === 0) { - return; - } - - const normalizedAddresses = addresses.map((item) => ({ - ...item, - address: item.address.toLowerCase(), - })); - - type RequestBody = { address: string; enabled: boolean }[]; - const body: RequestBody = normalizedAddresses; - await makeApiCall( - bearerToken, - TRIGGER_API_NOTIFICATIONS_ENDPOINT(env), - 'POST', - body, - ) - .then(() => notificationsConfigCache.set(normalizedAddresses)) - .catch(() => null); -} - /** * Fetches on-chain notifications for the given addresses * @@ -143,7 +98,7 @@ export async function updateOnChainNotifications( * @param locale - to generate translated notifications * @param platform - filter notifications for specific platforms ('extension' | 'mobile') * @param env - the environment to use for the API call - * @returns A promise that resolves to an array of NormalisedAPINotification objects. If no notifications are enabled or an error occurs, it may return an empty array. + * @returns An array of {@link NormalisedAPINotification}. Returns an empty array on transport or parse errors. */ export async function getAPINotifications( bearerToken: string, diff --git a/packages/notification-services-controller/src/index.ts b/packages/notification-services-controller/src/index.ts index 0d58fe030c..f2985ab0fb 100644 --- a/packages/notification-services-controller/src/index.ts +++ b/packages/notification-services-controller/src/index.ts @@ -1,2 +1,6 @@ export * as NotificationServicesController from './NotificationServicesController'; export * as NotificationServicesPushController from './NotificationServicesPushController'; +export { + DEFAULT_PERPS_PREFERENCES, + DEFAULT_SOCIAL_AI_PREFERENCES, +} from './NotificationServicesController'; diff --git a/packages/notification-services-controller/tsconfig.build.json b/packages/notification-services-controller/tsconfig.build.json index 5bc179c42c..b3b440974e 100644 --- a/packages/notification-services-controller/tsconfig.build.json +++ b/packages/notification-services-controller/tsconfig.build.json @@ -7,6 +7,9 @@ "skipLibCheck": true }, "references": [ + { + "path": "../authenticated-user-storage/tsconfig.build.json" + }, { "path": "../base-controller/tsconfig.build.json" }, diff --git a/packages/notification-services-controller/tsconfig.json b/packages/notification-services-controller/tsconfig.json index f846578229..3b73a515be 100644 --- a/packages/notification-services-controller/tsconfig.json +++ b/packages/notification-services-controller/tsconfig.json @@ -4,6 +4,9 @@ "baseUrl": "./" }, "references": [ + { + "path": "../authenticated-user-storage" + }, { "path": "../base-controller" }, diff --git a/yarn.lock b/yarn.lock index dfef61c543..861754e141 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4826,6 +4826,7 @@ __metadata: "@contentful/rich-text-html-renderer": "npm:^16.5.2" "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" + "@metamask/authenticated-user-storage": "npm:^1.0.1" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/controller-utils": "npm:^12.1.0" From 0ff0c065f6fda6ca76fbaff47d8a94ef7c9099ed Mon Sep 17 00:00:00 2001 From: mar <72634565+mindofmar@users.noreply.github.com> Date: Wed, 13 May 2026 11:12:08 -0500 Subject: [PATCH 3/5] feat: path-based scanning of urls (#8662) ## Explanation Why: Dapp scanning now supports path-level dapp scanning. Without this client-side change, the API never receives paths and the path-scanning capability goes unused. ## References Fixes: https://consensyssoftware.atlassian.net/jira/software/c/projects/PSAFE/boards/1950?selectedIssue=PSAFE-419 Extension PR: https://github.com/MetaMask/metamask-extension/pull/42311 ## Screenshots I've ran MetaMask Extension locally with these changes. Paths are now included in the API request. image ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Modifies `scanUrl` request/caching semantics to sometimes key on `hostname+pathname`, which can change phishing detection outcomes and cache behavior for gateway domains and could affect API load if misclassified. > > **Overview** > **Adds path-aware phishing URL scanning for shared gateway hosts.** `PhishingController.scanUrl` now sends `hostname+pathname` (instead of hostname-only) for a curated set of gateway root domains and subdomains, and caches results by this scan parameter. > > Introduces new utilities/constants (`PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS`, `isPhishingDetectionPathBasedHostname`, `getPhishingDetectionScanUrlParam`), exports them from `index.ts`, and updates tests/changelog to cover the new request format and per-path caching behavior. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 74ef4dc54ff0641e9167468e794f22714278aa68. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/phishing-controller/CHANGELOG.md | 4 ++ .../PhishingController-method-action-types.ts | 5 +- .../src/PhishingController.test.ts | 52 +++++++++++++++++- .../src/PhishingController.ts | 18 ++++--- packages/phishing-controller/src/index.ts | 5 ++ .../phishing-controller/src/utils.test.ts | 50 +++++++++++++++++ packages/phishing-controller/src/utils.ts | 54 +++++++++++++++++++ 7 files changed, 178 insertions(+), 10 deletions(-) diff --git a/packages/phishing-controller/CHANGELOG.md b/packages/phishing-controller/CHANGELOG.md index dcc64775a6..d5e3df80c6 100644 --- a/packages/phishing-controller/CHANGELOG.md +++ b/packages/phishing-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support path-based phishing lists (`blocklistPaths`, `whitelistPaths`) and path-aware URL scanning for shared gateways (for example IPFS gateways and `sites.google.com`) via `getPhishingDetectionScanUrlParam`, `isPhishingDetectionPathBasedHostname`, and `PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS` ([#8662](https://github.com/MetaMask/core/pull/8662)) + ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) diff --git a/packages/phishing-controller/src/PhishingController-method-action-types.ts b/packages/phishing-controller/src/PhishingController-method-action-types.ts index dfda91ec84..ed5d94cd2c 100644 --- a/packages/phishing-controller/src/PhishingController-method-action-types.ts +++ b/packages/phishing-controller/src/PhishingController-method-action-types.ts @@ -59,8 +59,9 @@ export type PhishingControllerBypassAction = { }; /** - * Scan a URL for phishing. It will only scan the hostname of the URL. It also only supports - * web URLs. + * Scan a URL for phishing. For most hosts only the hostname is sent to the API; for known + * shared gateways the pathname is included (see `PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS`). + * Only supports web URLs (`http:` / `https:`). * * @param url - The URL to scan. * @returns The phishing detection scan result. diff --git a/packages/phishing-controller/src/PhishingController.test.ts b/packages/phishing-controller/src/PhishingController.test.ts index 046be4253d..9e931217d7 100644 --- a/packages/phishing-controller/src/PhishingController.test.ts +++ b/packages/phishing-controller/src/PhishingController.test.ts @@ -2813,7 +2813,7 @@ describe('PhishingController', () => { it('should return a PhishingDetectionScanResult with a fetchError on timeout', async () => { const scope = nock(PHISHING_DETECTION_BASE_URL) .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) - .query({ url: testUrl }) + .query({ url: 'example.com' }) .delayConnection(10000) .reply(200, {}); @@ -2935,6 +2935,56 @@ describe('PhishingController', () => { expect(response).toMatchObject(mockResponse); expect(scope.isDone()).toBe(true); }); + + it('should send hostname and path for path-based gateways and cache per path', async () => { + const urlA = 'https://ipfs.io/ipfs/QmAAA'; + const urlB = 'https://ipfs.io/ipfs/QmBBB'; + + const scopeA = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: 'ipfs.io/ipfs/QmAAA' }) + .reply(200, { + recommendedAction: RecommendedAction.Warn, + }); + + const scopeB = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: 'ipfs.io/ipfs/QmBBB' }) + .reply(200, { + recommendedAction: RecommendedAction.Block, + }); + + const fetchSpy = jest.spyOn(global, 'fetch'); + + const resultA1 = await rootMessenger.call( + 'PhishingController:scanUrl', + urlA, + ); + const resultB = await rootMessenger.call( + 'PhishingController:scanUrl', + urlB, + ); + const resultA2 = await rootMessenger.call( + 'PhishingController:scanUrl', + urlA, + ); + + expect(resultA1).toMatchObject({ + hostname: 'ipfs.io', + recommendedAction: RecommendedAction.Warn, + }); + expect(resultB).toMatchObject({ + hostname: 'ipfs.io', + recommendedAction: RecommendedAction.Block, + }); + expect(resultA2).toStrictEqual(resultA1); + + expect(scopeA.isDone()).toBe(true); + expect(scopeB.isDone()).toBe(true); + expect(fetchSpy).toHaveBeenCalledTimes(2); + + fetchSpy.mockRestore(); + }); }); describe('bulkScanUrls', () => { diff --git a/packages/phishing-controller/src/PhishingController.ts b/packages/phishing-controller/src/PhishingController.ts index a7821fc454..2619313124 100644 --- a/packages/phishing-controller/src/PhishingController.ts +++ b/packages/phishing-controller/src/PhishingController.ts @@ -48,6 +48,7 @@ import { getHostnameFromUrl, roundToNearestMinute, getHostnameFromWebUrl, + getPhishingDetectionScanUrlParam, buildCacheKey, splitCacheHits, resolveChainName, @@ -910,15 +911,16 @@ export class PhishingController extends BaseController< } /** - * Scan a URL for phishing. It will only scan the hostname of the URL. It also only supports - * web URLs. + * Scan a URL for phishing. For most hosts only the hostname is sent to the API; for known + * shared gateways the pathname is included (see `PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS`). + * Only supports web URLs (`http:` / `https:`). * * @param url - The URL to scan. * @returns The phishing detection scan result. */ async scanUrl(url: string): Promise { - const [hostname, ok] = getHostnameFromWebUrl(url); - if (!ok) { + const [scanUrlParam, scanParamOk] = getPhishingDetectionScanUrlParam(url); + if (!scanParamOk) { return { hostname: '', recommendedAction: RecommendedAction.None, @@ -926,7 +928,9 @@ export class PhishingController extends BaseController< }; } - const cachedResult = this.#urlScanCache.get(hostname); + const [hostname] = getHostnameFromWebUrl(url); + + const cachedResult = this.#urlScanCache.get(scanUrlParam); if (cachedResult) { return cachedResult; } @@ -934,7 +938,7 @@ export class PhishingController extends BaseController< const apiResponse = await safelyExecuteWithTimeout( async () => { const res = await fetch( - `${PHISHING_DETECTION_BASE_URL}/${PHISHING_DETECTION_SCAN_ENDPOINT}?url=${encodeURIComponent(hostname)}`, + `${PHISHING_DETECTION_BASE_URL}/${PHISHING_DETECTION_SCAN_ENDPOINT}?url=${encodeURIComponent(scanUrlParam)}`, { method: 'GET', headers: { @@ -974,7 +978,7 @@ export class PhishingController extends BaseController< recommendedAction: apiResponse.recommendedAction, }; - this.#urlScanCache.set(hostname, result); + this.#urlScanCache.set(scanUrlParam, result); return result; } diff --git a/packages/phishing-controller/src/index.ts b/packages/phishing-controller/src/index.ts index e4d44b72d8..5233dbad7a 100644 --- a/packages/phishing-controller/src/index.ts +++ b/packages/phishing-controller/src/index.ts @@ -29,6 +29,11 @@ export { ApprovalFeatureType, } from './types'; export type { CacheEntry } from './CacheManager'; +export { + PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS, + getPhishingDetectionScanUrlParam, + isPhishingDetectionPathBasedHostname, +} from './utils'; export type { PhishingControllerMaybeUpdateStateAction, diff --git a/packages/phishing-controller/src/utils.test.ts b/packages/phishing-controller/src/utils.test.ts index 83b14756f8..882d7c3089 100644 --- a/packages/phishing-controller/src/utils.test.ts +++ b/packages/phishing-controller/src/utils.test.ts @@ -10,6 +10,8 @@ import { getHostnameAndPathComponents, getHostnameFromUrl, getHostnameFromWebUrl, + getPhishingDetectionScanUrlParam, + isPhishingDetectionPathBasedHostname, matchPartsAgainstList, processConfigs, processDomainList, @@ -981,6 +983,54 @@ describe('getHostnameFromWebUrl', () => { ); }); +describe('isPhishingDetectionPathBasedHostname', () => { + it('returns true for registered roots and subdomains', () => { + expect(isPhishingDetectionPathBasedHostname('ipfs.io')).toBe(true); + expect(isPhishingDetectionPathBasedHostname('gateway.ipfs.io')).toBe(true); + expect(isPhishingDetectionPathBasedHostname('dweb.link')).toBe(true); + expect(isPhishingDetectionPathBasedHostname('sites.google.com')).toBe(true); + }); + + it('is case-insensitive', () => { + expect(isPhishingDetectionPathBasedHostname('IPFS.IO')).toBe(true); + expect(isPhishingDetectionPathBasedHostname('Gateway.IPFS.IO')).toBe(true); + }); + + it('returns false for unrelated hosts', () => { + expect(isPhishingDetectionPathBasedHostname('example.com')).toBe(false); + expect(isPhishingDetectionPathBasedHostname('evil-ipfs.io')).toBe(false); + }); +}); + +describe('getPhishingDetectionScanUrlParam', () => { + it('returns hostname only for non-gateway hosts', () => { + expect( + getPhishingDetectionScanUrlParam('https://example.com/path?q=1#h'), + ).toStrictEqual(['example.com', true]); + }); + + it('returns hostname plus path for path-based gateway hosts', () => { + expect( + getPhishingDetectionScanUrlParam( + 'https://ipfs.io/ipfs/QmAAA/foo?x=1#frag', + ), + ).toStrictEqual(['ipfs.io/ipfs/QmAAA/foo', true]); + }); + + it('does not append path when pathname is /', () => { + expect( + getPhishingDetectionScanUrlParam('https://dweb.link/'), + ).toStrictEqual(['dweb.link', true]); + }); + + it('returns ok false for invalid web URLs', () => { + expect(getPhishingDetectionScanUrlParam('not-a-url')).toStrictEqual([ + '', + false, + ]); + }); +}); + /** * Extracts the domain name (e.g., example.com) from a given hostname. * diff --git a/packages/phishing-controller/src/utils.ts b/packages/phishing-controller/src/utils.ts index a74506b624..ff1ac2e7a7 100644 --- a/packages/phishing-controller/src/utils.ts +++ b/packages/phishing-controller/src/utils.ts @@ -365,6 +365,60 @@ export const getHostnameFromWebUrl = (url: string): [string, boolean] => { return [hostname || '', Boolean(hostname)]; }; +/** + * Hosts where PDS single-URL scans include the URL path (shared gateways / hosts where many sites + * share one origin). For all other hosts, only the hostname is sent. + */ +export const PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS = [ + 'ipfs.io', + 'dweb.link', + 'cf-ipfs.com', + 'cloudflare-ipfs.com', + 'irys.xyz', + 'sites.google.com', +] as const; + +/** + * @param hostname - Lowercase normalization is applied for matching registered roots and subdomains. + * @returns Whether {@link getPhishingDetectionScanUrlParam} appends pathname for this hostname. + */ +export function isPhishingDetectionPathBasedHostname( + hostname: string, +): boolean { + const normalizedHost = hostname.toLowerCase(); + return PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS.some( + (root) => normalizedHost === root || normalizedHost.endsWith(`.${root}`), + ); +} + +/** + * Builds the `url` query parameter for {@link PhishingController.scanUrl}. For hosts in + * {@link PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS} (and their subdomains), the value is hostname + * plus pathname, without protocol, query, or fragment. For all other hosts, only hostname is used. + * + * @param url - A web URL string (must use `http:` or `https:` — same rules as {@link getHostnameFromWebUrl}). + * @returns A tuple of `[scanUrlParam, ok]` where `ok` is false when the URL is not a valid web URL. + */ +export const getPhishingDetectionScanUrlParam = ( + url: string, +): [scanUrlParam: string, ok: boolean] => { + const [hostname, ok] = getHostnameFromWebUrl(url); + if (!ok) { + return ['', false]; + } + + if (!isPhishingDetectionPathBasedHostname(hostname)) { + return [hostname, true]; + } + + // `getHostnameFromWebUrl` already required a successful `new URL(url)` parse. + const { pathname } = new URL(url); + const pathSuffix = pathname === '/' ? '' : pathname; + const scanUrlParam = pathSuffix ? `${hostname}${pathSuffix}` : hostname; + + return [scanUrlParam, true]; +}; + export const getPathnameFromUrl = (url: string): string => { try { const { pathname } = new URL(url); From ad4cdea1b778b818b9c9378789bc1b18f008a0c8 Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Wed, 13 May 2026 18:44:49 +0200 Subject: [PATCH 4/5] Release/981.0.0 (#8799) ## Explanation ## References ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Low risk: this is a release/version bookkeeping PR that mainly updates package versions, changelogs, and dependency ranges without changing runtime logic. > > **Overview** > Bumps the root monorepo version to `981.0.0` and publishes new package versions for `@metamask/base-data-service` (`0.1.3`), `@metamask/react-data-query` (`0.2.1`), and `@metamask/eip-5792-middleware` (`3.0.4`). > > Updates consumers (`authenticated-user-storage`, `chomp-api-service`, `money-account-balance-service`, `react-data-query`, `sample-controllers`, `social-controllers`) to depend on `@metamask/base-data-service@^0.1.3`, and refreshes associated changelog entries and `yarn.lock` resolutions. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 78dd4583f0c1d10866c624605642e8f59bff0dea. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package.json | 2 +- packages/authenticated-user-storage/CHANGELOG.md | 1 + packages/authenticated-user-storage/package.json | 2 +- packages/base-data-service/CHANGELOG.md | 5 ++++- packages/base-data-service/package.json | 2 +- packages/chomp-api-service/CHANGELOG.md | 1 + packages/chomp-api-service/package.json | 2 +- packages/eip-5792-middleware/CHANGELOG.md | 5 ++++- packages/eip-5792-middleware/package.json | 2 +- .../money-account-balance-service/CHANGELOG.md | 4 ++++ .../money-account-balance-service/package.json | 2 +- packages/react-data-query/CHANGELOG.md | 7 +++++-- packages/react-data-query/package.json | 4 ++-- packages/sample-controllers/CHANGELOG.md | 4 ++++ packages/sample-controllers/package.json | 2 +- packages/social-controllers/CHANGELOG.md | 1 + packages/social-controllers/package.json | 2 +- yarn.lock | 14 +++++++------- 18 files changed, 41 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 5353dcae47..88e37eea96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "980.0.0", + "version": "981.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/authenticated-user-storage/CHANGELOG.md b/packages/authenticated-user-storage/CHANGELOG.md index 0c439f88c3..a12ff6b149 100644 --- a/packages/authenticated-user-storage/CHANGELOG.md +++ b/packages/authenticated-user-storage/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) +- Bump `@metamask/base-data-service` from `^0.1.2` to `^0.1.3` ([#8799](https://github.com/MetaMask/core/pull/8799)) - **BREAKING:** Replace `enabled` by `inAppNotificationsEnabled` and `pushNotificationsEnabled` in all the `NotificationPreferences` type fields and validation to match the API payload. ([#8784](https://github.com/MetaMask/core/pull/8784)) ## [1.0.1] diff --git a/packages/authenticated-user-storage/package.json b/packages/authenticated-user-storage/package.json index 78374b1484..1e31d30501 100644 --- a/packages/authenticated-user-storage/package.json +++ b/packages/authenticated-user-storage/package.json @@ -53,7 +53,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-data-service": "^0.1.2", + "@metamask/base-data-service": "^0.1.3", "@metamask/controller-utils": "^12.1.0", "@metamask/messenger": "^1.2.0", "@metamask/superstruct": "^3.1.0", diff --git a/packages/base-data-service/CHANGELOG.md b/packages/base-data-service/CHANGELOG.md index ff06bb7e9a..bb91762a5b 100644 --- a/packages/base-data-service/CHANGELOG.md +++ b/packages/base-data-service/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.3] + ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) @@ -30,7 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#8039](https://github.com/MetaMask/core/pull/8039), [#8292](https://github.com/MetaMask/core/pull/8292)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/base-data-service@0.1.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/base-data-service@0.1.3...HEAD +[0.1.3]: https://github.com/MetaMask/core/compare/@metamask/base-data-service@0.1.2...@metamask/base-data-service@0.1.3 [0.1.2]: https://github.com/MetaMask/core/compare/@metamask/base-data-service@0.1.1...@metamask/base-data-service@0.1.2 [0.1.1]: https://github.com/MetaMask/core/compare/@metamask/base-data-service@0.1.0...@metamask/base-data-service@0.1.1 [0.1.0]: https://github.com/MetaMask/core/releases/tag/@metamask/base-data-service@0.1.0 diff --git a/packages/base-data-service/package.json b/packages/base-data-service/package.json index 98d22500a5..fb2f4705df 100644 --- a/packages/base-data-service/package.json +++ b/packages/base-data-service/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/base-data-service", - "version": "0.1.2", + "version": "0.1.3", "description": "Provides utilities for building data services", "keywords": [ "Ethereum", diff --git a/packages/chomp-api-service/CHANGELOG.md b/packages/chomp-api-service/CHANGELOG.md index f0484419b1..d5b8a0690c 100644 --- a/packages/chomp-api-service/CHANGELOG.md +++ b/packages/chomp-api-service/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) +- Bump `@metamask/base-data-service` from `^0.1.2` to `^0.1.3` ([#8799](https://github.com/MetaMask/core/pull/8799)) ## [3.1.0] diff --git a/packages/chomp-api-service/package.json b/packages/chomp-api-service/package.json index 78d9e2330f..bc412c3a85 100644 --- a/packages/chomp-api-service/package.json +++ b/packages/chomp-api-service/package.json @@ -51,7 +51,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-data-service": "^0.1.2", + "@metamask/base-data-service": "^0.1.3", "@metamask/controller-utils": "^12.1.0", "@metamask/messenger": "^1.2.0", "@metamask/superstruct": "^3.1.0", diff --git a/packages/eip-5792-middleware/CHANGELOG.md b/packages/eip-5792-middleware/CHANGELOG.md index 8f90cd1513..2244fe5161 100644 --- a/packages/eip-5792-middleware/CHANGELOG.md +++ b/packages/eip-5792-middleware/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.0.4] + ### Changed - Bump `@metamask/keyring-controller` from `^25.3.0` to `^25.4.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) @@ -111,7 +113,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#6458](https://github.com/MetaMask/core/pull/6458)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/eip-5792-middleware@3.0.3...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/eip-5792-middleware@3.0.4...HEAD +[3.0.4]: https://github.com/MetaMask/core/compare/@metamask/eip-5792-middleware@3.0.3...@metamask/eip-5792-middleware@3.0.4 [3.0.3]: https://github.com/MetaMask/core/compare/@metamask/eip-5792-middleware@3.0.2...@metamask/eip-5792-middleware@3.0.3 [3.0.2]: https://github.com/MetaMask/core/compare/@metamask/eip-5792-middleware@3.0.1...@metamask/eip-5792-middleware@3.0.2 [3.0.1]: https://github.com/MetaMask/core/compare/@metamask/eip-5792-middleware@3.0.0...@metamask/eip-5792-middleware@3.0.1 diff --git a/packages/eip-5792-middleware/package.json b/packages/eip-5792-middleware/package.json index b1e707202d..d853a5d12b 100644 --- a/packages/eip-5792-middleware/package.json +++ b/packages/eip-5792-middleware/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eip-5792-middleware", - "version": "3.0.3", + "version": "3.0.4", "description": "Implements the JSON-RPC methods for sending multiple calls from the user's wallet, and checking their status, as referenced in EIP-5792", "keywords": [ "Ethereum", diff --git a/packages/money-account-balance-service/CHANGELOG.md b/packages/money-account-balance-service/CHANGELOG.md index 8c6a42e411..1d62b0ccdc 100644 --- a/packages/money-account-balance-service/CHANGELOG.md +++ b/packages/money-account-balance-service/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/base-data-service` from `^0.1.2` to `^0.1.3` ([#8799](https://github.com/MetaMask/core/pull/8799)) + ## [1.0.2] ### Changed diff --git a/packages/money-account-balance-service/package.json b/packages/money-account-balance-service/package.json index 47555750bc..187b1f5b4b 100644 --- a/packages/money-account-balance-service/package.json +++ b/packages/money-account-balance-service/package.json @@ -55,7 +55,7 @@ "dependencies": { "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", - "@metamask/base-data-service": "^0.1.2", + "@metamask/base-data-service": "^0.1.3", "@metamask/controller-utils": "^12.1.0", "@metamask/messenger": "^1.2.0", "@metamask/metamask-eth-abis": "^3.1.1", diff --git a/packages/react-data-query/CHANGELOG.md b/packages/react-data-query/CHANGELOG.md index 84dcdc0da0..3e75ea4b24 100644 --- a/packages/react-data-query/CHANGELOG.md +++ b/packages/react-data-query/CHANGELOG.md @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] + ### Changed -- Bump `@metamask/base-data-service` from `^0.1.0` to `^0.1.2` ([#8317](https://github.com/MetaMask/core/pull/8317), [#8755](https://github.com/MetaMask/core/pull/8755)) +- Bump `@metamask/base-data-service` from `^0.1.0` to `^0.1.3` ([#8317](https://github.com/MetaMask/core/pull/8317), [#8755](https://github.com/MetaMask/core/pull/8755), [#8799](https://github.com/MetaMask/core/pull/8799)) ## [0.2.0] @@ -24,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#8039](https://github.com/MetaMask/core/pull/8039), [#8292](https://github.com/MetaMask/core/pull/8292)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/react-data-query@0.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/react-data-query@0.2.1...HEAD +[0.2.1]: https://github.com/MetaMask/core/compare/@metamask/react-data-query@0.2.0...@metamask/react-data-query@0.2.1 [0.2.0]: https://github.com/MetaMask/core/compare/@metamask/react-data-query@0.1.0...@metamask/react-data-query@0.2.0 [0.1.0]: https://github.com/MetaMask/core/releases/tag/@metamask/react-data-query@0.1.0 diff --git a/packages/react-data-query/package.json b/packages/react-data-query/package.json index 69a28c1ffa..0344a4fa6c 100644 --- a/packages/react-data-query/package.json +++ b/packages/react-data-query/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/react-data-query", - "version": "0.2.0", + "version": "0.2.1", "description": "Provides React utilities for consuming data services", "keywords": [ "Ethereum", @@ -51,7 +51,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-data-service": "^0.1.2", + "@metamask/base-data-service": "^0.1.3", "@metamask/utils": "^11.9.0", "@tanstack/query-core": "^4.43.0", "@tanstack/react-query": "^4.43.0" diff --git a/packages/sample-controllers/CHANGELOG.md b/packages/sample-controllers/CHANGELOG.md index 66cdc48c94..301084bef9 100644 --- a/packages/sample-controllers/CHANGELOG.md +++ b/packages/sample-controllers/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/base-data-service` from `^0.1.2` to `^0.1.3` ([#8799](https://github.com/MetaMask/core/pull/8799)) + ## [5.0.1] ### Changed diff --git a/packages/sample-controllers/package.json b/packages/sample-controllers/package.json index af86d7edea..8005c5063b 100644 --- a/packages/sample-controllers/package.json +++ b/packages/sample-controllers/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", - "@metamask/base-data-service": "^0.1.2", + "@metamask/base-data-service": "^0.1.3", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^32.0.0", "@metamask/superstruct": "^3.1.0", diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index 812fe69e53..39faa0dc12 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) - Bump `@metamask/profile-sync-controller` from `^28.0.2` to `^28.1.0` ([#8783](https://github.com/MetaMask/core/pull/8783)) +- Bump `@metamask/base-data-service` from `^0.1.2` to `^0.1.3` ([#8799](https://github.com/MetaMask/core/pull/8799)) ## [2.2.1] diff --git a/packages/social-controllers/package.json b/packages/social-controllers/package.json index f0d194c423..c6067d6868 100644 --- a/packages/social-controllers/package.json +++ b/packages/social-controllers/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", - "@metamask/base-data-service": "^0.1.2", + "@metamask/base-data-service": "^0.1.3", "@metamask/controller-utils": "^12.1.0", "@metamask/messenger": "^1.2.0", "@metamask/profile-sync-controller": "^28.1.0", diff --git a/yarn.lock b/yarn.lock index 861754e141..3f5ff36712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2931,7 +2931,7 @@ __metadata: resolution: "@metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage" dependencies: "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/base-data-service": "npm:^0.1.2" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/controller-utils": "npm:^12.1.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -2992,7 +2992,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/base-data-service@npm:^0.1.2, @metamask/base-data-service@workspace:packages/base-data-service": +"@metamask/base-data-service@npm:^0.1.3, @metamask/base-data-service@workspace:packages/base-data-service": version: 0.0.0-use.local resolution: "@metamask/base-data-service@workspace:packages/base-data-service" dependencies: @@ -3153,7 +3153,7 @@ __metadata: resolution: "@metamask/chomp-api-service@workspace:packages/chomp-api-service" dependencies: "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/base-data-service": "npm:^0.1.2" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/controller-utils": "npm:^12.1.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -4490,7 +4490,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/base-data-service": "npm:^0.1.2" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/controller-utils": "npm:^12.1.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" @@ -5231,7 +5231,7 @@ __metadata: resolution: "@metamask/react-data-query@workspace:packages/react-data-query" dependencies: "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/base-data-service": "npm:^0.1.2" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/utils": "npm:^11.9.0" "@tanstack/query-core": "npm:^4.43.0" "@tanstack/react-query": "npm:^4.43.0" @@ -5297,7 +5297,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/base-data-service": "npm:^0.1.2" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/controller-utils": "npm:^12.1.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^32.0.0" @@ -5606,7 +5606,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/base-data-service": "npm:^0.1.2" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/controller-utils": "npm:^12.1.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/profile-sync-controller": "npm:^28.1.0" From c93c3c1a745e40539c2120568ac7e95212f328e2 Mon Sep 17 00:00:00 2001 From: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Date: Wed, 13 May 2026 21:40:55 +0200 Subject: [PATCH 5/5] Release/982.0.0 (#8802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation ## References ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Primarily release metadata and dependency version bumps; the main risk is downstream breakage for consumers that haven’t yet adapted to the `@metamask/authenticated-user-storage@2.0.0` breaking type changes. > > **Overview** > Bumps the monorepo release version to `982.0.0` and publishes new versions for `@metamask/authenticated-user-storage` (`2.0.0`), `@metamask/notification-services-controller` (`24.0.0`), and `@metamask/money-account-upgrade-controller` (`2.0.2`). > > Updates `money-account-upgrade-controller` and `notification-services-controller` to depend on `@metamask/authenticated-user-storage@^2.0.0`, with corresponding changelog entries and `yarn.lock` updates. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a46578815ca94fbb9a5d8090df7346adf76096c6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package.json | 2 +- .../authenticated-user-storage/CHANGELOG.md | 5 ++++- .../authenticated-user-storage/package.json | 2 +- .../CHANGELOG.md | 9 ++++++++- .../package.json | 4 ++-- .../CHANGELOG.md | 17 ++++++++--------- .../package.json | 4 ++-- yarn.lock | 6 +++--- 8 files changed, 29 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 88e37eea96..29a8b79c2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "981.0.0", + "version": "982.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/authenticated-user-storage/CHANGELOG.md b/packages/authenticated-user-storage/CHANGELOG.md index a12ff6b149..f01e3dfe14 100644 --- a/packages/authenticated-user-storage/CHANGELOG.md +++ b/packages/authenticated-user-storage/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.0] + ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) @@ -32,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING**: Rename `SocialAIPreference.traderProfileIds` to `mutedTraderProfileIds` in types and notification-preferences validation to match the API payload. ([#8536](https://github.com/MetaMask/core/pull/8536)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/authenticated-user-storage@1.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/authenticated-user-storage@2.0.0...HEAD +[2.0.0]: https://github.com/MetaMask/core/compare/@metamask/authenticated-user-storage@1.0.1...@metamask/authenticated-user-storage@2.0.0 [1.0.1]: https://github.com/MetaMask/core/compare/@metamask/authenticated-user-storage@1.0.0...@metamask/authenticated-user-storage@1.0.1 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/authenticated-user-storage@1.0.0 diff --git a/packages/authenticated-user-storage/package.json b/packages/authenticated-user-storage/package.json index 1e31d30501..c53700ae43 100644 --- a/packages/authenticated-user-storage/package.json +++ b/packages/authenticated-user-storage/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/authenticated-user-storage", - "version": "1.0.1", + "version": "2.0.0", "description": "SDK for authenticated (non-encrypted) user storage endpoints", "keywords": [ "Ethereum", diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md index ea90a8a858..d6303187bb 100644 --- a/packages/money-account-upgrade-controller/CHANGELOG.md +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.2] + +### Changed + +- Bump `@metamask/authenticated-user-storage` from `^1.0.1` to `^2.0.0` ([#8802](https://github.com/MetaMask/core/pull/8802)) + ## [2.0.1] ### Changed @@ -85,7 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `MoneyAccountUpgradeController` with `upgradeAccount` method ([#8426](https://github.com/MetaMask/core/pull/8426)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/money-account-upgrade-controller@2.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/money-account-upgrade-controller@2.0.2...HEAD +[2.0.2]: https://github.com/MetaMask/core/compare/@metamask/money-account-upgrade-controller@2.0.1...@metamask/money-account-upgrade-controller@2.0.2 [2.0.1]: https://github.com/MetaMask/core/compare/@metamask/money-account-upgrade-controller@2.0.0...@metamask/money-account-upgrade-controller@2.0.1 [2.0.0]: https://github.com/MetaMask/core/compare/@metamask/money-account-upgrade-controller@1.3.2...@metamask/money-account-upgrade-controller@2.0.0 [1.3.2]: https://github.com/MetaMask/core/compare/@metamask/money-account-upgrade-controller@1.3.1...@metamask/money-account-upgrade-controller@1.3.2 diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 961f715505..302452669b 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/money-account-upgrade-controller", - "version": "2.0.1", + "version": "2.0.2", "description": "MetaMask Money account upgrade controller", "keywords": [ "Ethereum", @@ -53,7 +53,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/authenticated-user-storage": "^1.0.1", + "@metamask/authenticated-user-storage": "^2.0.0", "@metamask/base-controller": "^9.1.0", "@metamask/chomp-api-service": "^3.1.0", "@metamask/delegation-controller": "^3.0.0", diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index ddf16704d3..147522beea 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [24.0.0] + ### Added - Add `productAnnouncementEnabled` to `NotificationServicesControllerEnableNotificationsOptions`. ([#8784](https://github.com/MetaMask/core/pull/8784)) @@ -21,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Marketing push notifications are initialized from `hasMarketingConsent`, while marketing in-app notifications are initialized from `productAnnouncementEnabled`. - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) - Bump `@metamask/profile-sync-controller` from `^28.0.2` to `^28.1.0` ([#8783](https://github.com/MetaMask/core/pull/8783)) +- Bump `@metamask/authenticated-user-storage` from `^1.0.1` to `^2.0.0` ([#8802](https://github.com/MetaMask/core/pull/8802)) ### Removed @@ -501,8 +504,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^18.0.0` to `^19.0.0` ([#4195](https://github.com/MetaMask/core/pull/4956)) -- **BREAKING:** Bump `@metamask/profile-sync-controller` peer dependency from `^1.0.0` to `^2.0.0` ([#4195](https://github.com/MetaMask/core/pull/4956)) +- **BREAKING:** Bump `@metamask/keyring-controller` peer dependency from `^18.0.0` to `^19.0.0` ([#4195](https://github.com/MetaMask/core/pull/4195)) +- **BREAKING:** Bump `@metamask/profile-sync-controller` peer dependency from `^1.0.0` to `^2.0.0` ([#4195](https://github.com/MetaMask/core/pull/4195)) ## [0.13.0] @@ -544,7 +547,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update the types described in `types/on-chain-notification/schema` and `types/on-chain-notification/on-chain-notification` ([#4818](https://github.com/MetaMask/core/pull/4818)) - adds new notifications: aave_v3_health_factor; ens_expiration; lido_staking_rewards; notional_loan_expiration; rocketpool_staking_rewards; spark_fi_health_factor - splits Wallet Notifications from Web 3 Notifications - - updated and added new notification mocks ([#4818](https://github.com/MetaMask/core/pull/4818)) - can be accessed through `@metamask/notification-services-controller/notification-services/mocks` @@ -623,7 +625,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ["Are the Types Wrong?"](https://arethetypeswrong.github.io/) tool as ["masquerading as CJS"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md). All of the ATTW checks now pass. -- Remove chunk files ([#4648](https://github.com/MetaMask/core/pull/4648)). +- Remove chunk files. ([#4648](https://github.com/MetaMask/core/pull/4648)) - Previously, the build tool we used to generate JavaScript files extracted common code to "chunk" files. While this was intended to make this package more tree-shakeable, it also made debugging more difficult for our @@ -716,7 +718,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - added catch statements in NotificationServicesController to silently fail push notifications ([#4536](https://github.com/MetaMask/core/pull/4536)) - - added checks to see feature announcement environments before fetching announcements ([#4530](https://github.com/MetaMask/core/pull/4530)) ### Removed @@ -728,11 +729,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - export `defaultState` for `NotificationServicesController` and `NotificationServicesPushController`. ([#4441](https://github.com/MetaMask/core/pull/4441)) - - export `NOTIFICATION_CHAINS_ID` which is a const-asserted version of `NOTIFICATION_CHAINS` ([#4441](https://github.com/MetaMask/core/pull/4441)) - - export `NOTIFICATION_NETWORK_CURRENCY_NAME` and `NOTIFICATION_NETWORK_CURRENCY_SYMBOL`. Allows consistent currency names and symbols for supported notification services ([#4441](https://github.com/MetaMask/core/pull/4441)) - - add `isPushIntegrated` as an optional env property in the `NotificationServicesController` constructor (defaults to true) ([#4441](https://github.com/MetaMask/core/pull/4441)) ### Fixed @@ -745,7 +743,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@23.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@24.0.0...HEAD +[24.0.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@23.1.1...@metamask/notification-services-controller@24.0.0 [23.1.1]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@23.1.0...@metamask/notification-services-controller@23.1.1 [23.1.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@23.0.1...@metamask/notification-services-controller@23.1.0 [23.0.1]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@23.0.0...@metamask/notification-services-controller@23.0.1 diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index 615bbe5186..92c98750d6 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/notification-services-controller", - "version": "23.1.1", + "version": "24.0.0", "description": "Manages New MetaMask decentralized Notification system", "keywords": [ "Ethereum", @@ -106,7 +106,7 @@ }, "dependencies": { "@contentful/rich-text-html-renderer": "^16.5.2", - "@metamask/authenticated-user-storage": "^1.0.1", + "@metamask/authenticated-user-storage": "^2.0.0", "@metamask/base-controller": "^9.1.0", "@metamask/controller-utils": "^12.1.0", "@metamask/keyring-controller": "^25.5.0", diff --git a/yarn.lock b/yarn.lock index 3f5ff36712..87c2892c8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2926,7 +2926,7 @@ __metadata: languageName: node linkType: hard -"@metamask/authenticated-user-storage@npm:^1.0.1, @metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage": +"@metamask/authenticated-user-storage@npm:^2.0.0, @metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage": version: 0.0.0-use.local resolution: "@metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage" dependencies: @@ -4541,7 +4541,7 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller" dependencies: - "@metamask/authenticated-user-storage": "npm:^1.0.1" + "@metamask/authenticated-user-storage": "npm:^2.0.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/chomp-api-service": "npm:^3.1.0" @@ -4826,7 +4826,7 @@ __metadata: "@contentful/rich-text-html-renderer": "npm:^16.5.2" "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask/authenticated-user-storage": "npm:^1.0.1" + "@metamask/authenticated-user-storage": "npm:^2.0.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/controller-utils": "npm:^12.1.0"