Skip to content

Commit 095f256

Browse files
feat: migration notification API from v2 to v3 (#7102)
## Explanation This is a large notification architecture API change but offers dynamic platform notifications <details><summary>Details</summary> <p> Devlog: https://www.loom.com/share/ea5fc1457c224b2988fadacbfc21ca2a </p> </details> Extension Preview: MetaMask/metamask-extension#37709 Mobile Preview: MetaMask/metamask-mobile#22539 ## References https://consensyssoftware.atlassian.net/browse/ASSETS-1301 <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## 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/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Migrates notifications from v2 to v3, adds platform notifications and locale-aware fetching, overhauls types/processors, and reduces auto-expiry to 30 days. > > - **Notifications API migration (v2 → v3)**: > - Endpoints: `/api/v3/notifications`, `/api/v3/notifications/mark-as-read`. > - Request body now `{ addresses: string[], locale, platform }`; responses include `notification_type` and nested `payload`. > - **New platform notifications**: > - Added `TRIGGER_TYPES.PLATFORM` and support in processors/push messaging. > - Platform entries use `template` with localized `title`, `body`, `image_url`, `cta`. > - **Type and processor overhaul**: > - Replace `OnChainRawNotification` with `NormalisedAPINotification` (union of on-chain/platform); add `UnprocessedRawNotification`. > - New OpenAPI v3 schema/types; updated guards (`isOnChainRawNotification`) and converters (`toRawAPINotification`). > - Replace `processOnChainNotification()` with `processAPINotifications()` and update notification mapping. > - **Service/Controller changes**: > - Rename services: `getOnChainNotifications*` → `getAPINotifications`/`getNotificationsApiConfigCached`. > - Controller fetch path updated to use v3 + locale via new constructor `env.locale`. > - Auto-expiry utility added; expiry reduced from 90 to 30 days. > - **Push handling**: > - Update web listeners to parse v3 payloads and platform notifications. > - Push message builder reads nested `payload` and platform `template`. > - **Tests/mocks**: > - Updated mocks to v3 shapes; revised tests across services/processors/controller to new API and types. > - **Changelog**: Documents breaking changes, function renames, request/response changes, and expiry policy. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 29ed5f1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent c26899a commit 095f256

34 files changed

+1295
-1601
lines changed

packages/notification-services-controller/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING:** Moved Notification API from v2 to v3 ([#7102](https://github.com/MetaMask/core/pull/7102))
13+
- API Endpoint Changes: Updated from `/api/v2/notifications` to `/api/v3/notifications` for listing notifications and marking as read
14+
- Request Format: The list notifications endpoint now expects `{ addresses: string[], locale?: string }` instead of `{ address: string }[]`
15+
- Response Structure: Notifications now include a `notification_type` field ('on-chain' or 'platform') and nested payload structure
16+
- On-chain notifications: data moved from root level to `payload.data`
17+
- Platform notifications: new type with `template` containing localized content (`title`, `body`, `image_url`, `cta`)
18+
- Type System Overhaul:
19+
- `OnChainRawNotification``NormalisedAPINotification` (union of on-chain and platform)
20+
- `UnprocessedOnChainRawNotification``UnprocessedRawNotification`
21+
- Removed specific DeFi notification types (Aave, ENS, Lido rewards, etc.) - now will be handled generically
22+
- Added `TRIGGER_TYPES.PLATFORM` for platform notifications
23+
- Function Signatures:
24+
- `getOnChainNotifications()``getAPINotifications()` with new `locale` parameter
25+
- `getOnChainNotificationsConfigCached()``getNotificationsApiConfigCached()`
26+
- `processOnChainNotification()``processAPINotifications()`
27+
- Service Imports: Update imports from `onchain-notifications` to `api-notifications`
28+
- Auto-expiry: Reduced from 90 days to 30 days for notification auto-expiry
29+
- Locale Support: Added locale parameter to controller constructor for localized server notifications
30+
1031
## [19.0.0]
1132

1233
### Changed

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { ADDRESS_1, ADDRESS_2 } from './__fixtures__/mockAddresses';
2020
import {
2121
mockGetOnChainNotificationsConfig,
2222
mockUpdateOnChainNotifications,
23-
mockGetOnChainNotifications,
23+
mockGetAPINotifications,
2424
mockFetchFeatureAnnouncementNotifications,
2525
mockMarkNotificationsAsRead,
2626
mockCreatePerpNotification,
@@ -594,7 +594,7 @@ describe('NotificationServicesController', () => {
594594
const mockOnChainNotificationsAPIResult = [
595595
createMockNotificationEthSent(),
596596
];
597-
const mockOnChainNotificationsAPI = mockGetOnChainNotifications({
597+
const mockOnChainNotificationsAPI = mockGetAPINotifications({
598598
status: 200,
599599
body: mockOnChainNotificationsAPIResult,
600600
});
@@ -705,7 +705,7 @@ describe('NotificationServicesController', () => {
705705

706706
// Mock APIs to fail
707707
mockFetchFeatureAnnouncementNotifications({ status: 500 });
708-
mockGetOnChainNotifications({ status: 500 });
708+
mockGetAPINotifications({ status: 500 });
709709

710710
const controller = arrangeController(messenger);
711711

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,24 @@ import type { AuthenticationController } from '@metamask/profile-sync-controller
2121
import { assert } from '@metamask/utils';
2222
import log from 'loglevel';
2323

24+
import type { NormalisedAPINotification } from '.';
2425
import { TRIGGER_TYPES } from './constants/notification-schema';
2526
import {
2627
processAndFilterNotifications,
2728
safeProcessNotification,
2829
} from './processors/process-notifications';
29-
import * as FeatureNotifications from './services/feature-announcements';
30-
import * as OnChainNotifications from './services/onchain-notifications';
30+
import {
31+
getAPINotifications,
32+
getNotificationsApiConfigCached,
33+
markNotificationsAsRead,
34+
updateOnChainNotifications,
35+
} from './services/api-notifications';
36+
import { getFeatureAnnouncementNotifications } from './services/feature-announcements';
3137
import { createPerpOrderNotification } from './services/perp-notifications';
3238
import type {
3339
INotification,
3440
MarkAsReadNotificationsParam,
3541
} from './types/notification/notification';
36-
import type { OnChainRawNotification } from './types/on-chain-notification/on-chain-notification';
3742
import type { OrderInput } from './types/perps';
3843
import type {
3944
NotificationServicesPushControllerEnablePushNotificationsAction,
@@ -500,6 +505,8 @@ export default class NotificationServicesController extends BaseController<
500505
},
501506
};
502507

508+
readonly #locale: () => string;
509+
503510
readonly #featureAnnouncementEnv: FeatureAnnouncementEnv;
504511

505512
/**
@@ -510,7 +517,7 @@ export default class NotificationServicesController extends BaseController<
510517
* @param args.state - Initial state to set on this controller.
511518
* @param args.env - environment variables for a given controller.
512519
* @param args.env.featureAnnouncements - env variables for feature announcements.
513-
* @param args.env.isPushIntegrated - toggle push notifications on/off if client has integrated them.
520+
* @param args.env.locale - users locale for better dynamic server notifications
514521
*/
515522
constructor({
516523
messenger,
@@ -521,7 +528,7 @@ export default class NotificationServicesController extends BaseController<
521528
state?: Partial<NotificationServicesControllerState>;
522529
env: {
523530
featureAnnouncements: FeatureAnnouncementEnv;
524-
isPushIntegrated?: boolean;
531+
locale?: () => string;
525532
};
526533
}) {
527534
super({
@@ -532,6 +539,7 @@ export default class NotificationServicesController extends BaseController<
532539
});
533540

534541
this.#featureAnnouncementEnv = env.featureAnnouncements;
542+
this.#locale = env.locale ?? (() => 'en');
535543
this.#registerMessageHandlers();
536544
this.#clearLoadingStates();
537545
}
@@ -694,11 +702,10 @@ export default class NotificationServicesController extends BaseController<
694702
try {
695703
const { bearerToken } = await this.#getBearerToken();
696704
const { accounts } = this.#accounts.listAccounts();
697-
const addressesWithNotifications =
698-
await OnChainNotifications.getOnChainNotificationsConfigCached(
699-
bearerToken,
700-
accounts,
701-
);
705+
const addressesWithNotifications = await getNotificationsApiConfigCached(
706+
bearerToken,
707+
accounts,
708+
);
702709
const addresses = addressesWithNotifications
703710
.filter((a) => Boolean(a.enabled))
704711
.map((a) => a.address);
@@ -725,11 +732,10 @@ export default class NotificationServicesController extends BaseController<
725732

726733
// Retrieve user storage
727734
const { bearerToken } = await this.#getBearerToken();
728-
const addressesWithNotifications =
729-
await OnChainNotifications.getOnChainNotificationsConfigCached(
730-
bearerToken,
731-
accounts,
732-
);
735+
const addressesWithNotifications = await getNotificationsApiConfigCached(
736+
bearerToken,
737+
accounts,
738+
);
733739

734740
const result: Record<string, boolean> = {};
735741
addressesWithNotifications.forEach((a) => {
@@ -788,11 +794,10 @@ export default class NotificationServicesController extends BaseController<
788794
const { accounts } = this.#accounts.listAccounts();
789795

790796
// 1. See if has enabled notifications before
791-
const addressesWithNotifications =
792-
await OnChainNotifications.getOnChainNotificationsConfigCached(
793-
bearerToken,
794-
accounts,
795-
);
797+
const addressesWithNotifications = await getNotificationsApiConfigCached(
798+
bearerToken,
799+
accounts,
800+
);
796801

797802
// Notifications API can return array with addresses set to false
798803
// So assert that at least one address is enabled
@@ -802,7 +807,7 @@ export default class NotificationServicesController extends BaseController<
802807

803808
// 2. Enable Notifications (if no accounts subscribed or we are resetting)
804809
if (accountsWithNotifications.length === 0 || opts?.resetNotifications) {
805-
await OnChainNotifications.updateOnChainNotifications(
810+
await updateOnChainNotifications(
806811
bearerToken,
807812
accounts.map((address) => ({ address, enabled: true })),
808813
);
@@ -903,7 +908,7 @@ export default class NotificationServicesController extends BaseController<
903908
const { bearerToken } = await this.#getBearerToken();
904909

905910
// Delete these UUIDs (Mutates User Storage)
906-
await OnChainNotifications.updateOnChainNotifications(
911+
await updateOnChainNotifications(
907912
bearerToken,
908913
accounts.map((address) => ({ address, enabled: false })),
909914
);
@@ -935,7 +940,7 @@ export default class NotificationServicesController extends BaseController<
935940
this.#updateUpdatingAccountsState(accounts);
936941

937942
const { bearerToken } = await this.#getBearerToken();
938-
await OnChainNotifications.updateOnChainNotifications(
943+
await updateOnChainNotifications(
939944
bearerToken,
940945
accounts.map((address) => ({ address, enabled: true })),
941946
);
@@ -970,31 +975,29 @@ export default class NotificationServicesController extends BaseController<
970975
// Raw Feature Notifications
971976
const rawAnnouncements =
972977
isGlobalNotifsEnabled && this.state.isFeatureAnnouncementsEnabled
973-
? await FeatureNotifications.getFeatureAnnouncementNotifications(
978+
? await getFeatureAnnouncementNotifications(
974979
this.#featureAnnouncementEnv,
975980
previewToken,
976981
).catch(() => [])
977982
: [];
978983

979984
// Raw On Chain Notifications
980-
const rawOnChainNotifications: OnChainRawNotification[] = [];
985+
const rawOnChainNotifications: NormalisedAPINotification[] = [];
981986
if (isGlobalNotifsEnabled) {
982987
try {
983988
const { bearerToken } = await this.#getBearerToken();
984989
const { accounts } = this.#accounts.listAccounts();
985990
const addressesWithNotifications = (
986-
await OnChainNotifications.getOnChainNotificationsConfigCached(
987-
bearerToken,
988-
accounts,
989-
)
991+
await getNotificationsApiConfigCached(bearerToken, accounts)
990992
)
991993
.filter((a) => Boolean(a.enabled))
992994
.map((a) => a.address);
993-
const notifications =
994-
await OnChainNotifications.getOnChainNotifications(
995-
bearerToken,
996-
addressesWithNotifications,
997-
).catch(() => []);
995+
const notifications = await getAPINotifications(
996+
bearerToken,
997+
addressesWithNotifications,
998+
this.#locale(),
999+
this.#featureAnnouncementEnv.platform,
1000+
).catch(() => []);
9981001
rawOnChainNotifications.push(...notifications);
9991002
} catch {
10001003
// Do nothing
@@ -1165,7 +1168,7 @@ export default class NotificationServicesController extends BaseController<
11651168
onchainNotificationIds = onChainNotifications.map(
11661169
(notification) => notification.id,
11671170
);
1168-
await OnChainNotifications.markNotificationsAsRead(
1171+
await markNotificationsAsRead(
11691172
bearerToken,
11701173
onchainNotificationIds,
11711174
).catch(() => {

packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const mockGetOnChainNotificationsConfig = (mockReply?: MockReply) => {
4949
return mockEndpoint;
5050
};
5151

52-
export const mockGetOnChainNotifications = (mockReply?: MockReply) => {
52+
export const mockGetAPINotifications = (mockReply?: MockReply) => {
5353
const mockResponse = getMockListNotificationsResponse();
5454
const reply = mockReply ?? { status: 200, body: mockResponse.response };
5555

packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,11 @@ export enum TRIGGER_TYPES {
1515
ERC721_RECEIVED = 'erc721_received',
1616
ERC1155_SENT = 'erc1155_sent',
1717
ERC1155_RECEIVED = 'erc1155_received',
18-
AAVE_V3_HEALTH_FACTOR = 'aave_v3_health_factor',
19-
ENS_EXPIRATION = 'ens_expiration',
20-
LIDO_STAKING_REWARDS = 'lido_staking_rewards',
21-
ROCKETPOOL_STAKING_REWARDS = 'rocketpool_staking_rewards',
22-
NOTIONAL_LOAN_EXPIRATION = 'notional_loan_expiration',
23-
SPARK_FI_HEALTH_FACTOR = 'spark_fi_health_factor',
2418
SNAP = 'snap',
19+
PLATFORM = 'platform',
2520
}
2621

27-
export const TRIGGER_TYPES_WALLET_SET: Set<string> = new Set([
22+
export const NOTIFICATION_API_TRIGGER_TYPES_SET: Set<string> = new Set([
2823
TRIGGER_TYPES.METAMASK_SWAP_COMPLETED,
2924
TRIGGER_TYPES.ERC20_SENT,
3025
TRIGGER_TYPES.ERC20_RECEIVED,
@@ -40,13 +35,8 @@ export const TRIGGER_TYPES_WALLET_SET: Set<string> = new Set([
4035
TRIGGER_TYPES.ERC721_RECEIVED,
4136
TRIGGER_TYPES.ERC1155_SENT,
4237
TRIGGER_TYPES.ERC1155_RECEIVED,
43-
]) satisfies Set<Exclude<TRIGGER_TYPES, TRIGGER_TYPES.FEATURES_ANNOUNCEMENT>>;
44-
45-
export enum TRIGGER_TYPES_GROUPS {
46-
RECEIVED = 'received',
47-
SENT = 'sent',
48-
DEFI = 'defi',
49-
}
38+
TRIGGER_TYPES.PLATFORM,
39+
]);
5040

5141
export const NOTIFICATION_CHAINS_ID = {
5242
ETHEREUM: '1',

0 commit comments

Comments
 (0)