Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/authenticated-user-storage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `getAssetsWatchlist` and `setAssetsWatchlist` methods to `AuthenticatedUserStorageService` for managing the authenticated user's assets-watchlist, along with corresponding messenger actions (`AuthenticatedUserStorageService:getAssetsWatchlist`, `AuthenticatedUserStorageService:setAssetsWatchlist`), the `AssetsWatchlistBlob` type, and the `ASSETS_WATCHLIST_MAX_ASSETS` constant ([#8836](https://github.com/MetaMask/core/pull/8836))
- `getAssetsWatchlist` returns the assets-watchlist blob or `null` on 404, mirroring `getNotificationPreferences`.
- `setAssetsWatchlist` writes the full blob and enforces a maximum of `ASSETS_WATCHLIST_MAX_ASSETS` (100) assets before sending the request, via a superstruct `size` constraint on the write-side schema.
- Add `AgenticCliPreference` type and optional `agenticCli` field to `NotificationPreferences` for Agentic CLI notification preferences ([#8933](https://github.com/MetaMask/core/pull/8933))
- `agenticCli` is optional on the type for this release; the next major release should make it required.
- `getNotificationPreferences` backfills legacy blobs that omit `agenticCli` with `DEFAULT_AGENTIC_CLI_PREFERENCES`, then validates the result against the full schema.
- `putNotificationPreferences` relies on the TypeScript type for write shape; no runtime validation is performed on PUT.
- Add `DEFAULT_AGENTIC_CLI_PREFERENCES` for Agentic CLI notification preferences ([#8933](https://github.com/MetaMask/core/pull/8933))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export type AuthenticatedUserStorageServiceRevokeDelegationAction = {
/**
* Returns the notification preferences for the authenticated user.
*
* Legacy payloads that omit `agenticCli` are coerced with
* {@link DEFAULT_AGENTIC_CLI_PREFERENCES} on read. When this method returns
* a non-`null` value, `agenticCli` is always present (backfilled), even
* though {@link NotificationPreferences} marks it optional until the next
* major release.
*
* @returns The notification preferences object, or `null` if none have been
* set (404).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
MOCK_DELEGATION_RESPONSE,
MOCK_DELEGATION_SUBMISSION,
MOCK_INVALID_ASSETS_WATCHLIST_BLOB,
MOCK_LEGACY_NOTIFICATION_PREFERENCES,
MOCK_NOTIFICATION_PREFERENCES,
MOCK_ASSETS_WATCHLIST_BLOB,
MOCK_ASSETS_WATCHLIST_URL,
Expand All @@ -30,7 +31,10 @@ import {
} from './authenticated-user-storage';
import type { Environment } from './env';
import { getUserStorageApiUrl } from './env';
import { ASSETS_WATCHLIST_MAX_ASSETS } from './validators';
import {
ASSETS_WATCHLIST_MAX_ASSETS,
DEFAULT_AGENTIC_CLI_PREFERENCES,
} from './validators';

const MOCK_ACCESS_TOKEN = 'mock-access-token';

Expand Down Expand Up @@ -199,6 +203,41 @@ describe('AuthenticatedUserStorageService', () => {
'Failed to get notification preferences: 500',
);
});

it('coerces legacy payloads that omit agenticCli', async () => {
handleMockGetNotificationPreferences({
status: 200,
body: MOCK_LEGACY_NOTIFICATION_PREFERENCES,
});
const { service } = createService();

const result = await service.getNotificationPreferences();

expect(result).toStrictEqual({
...MOCK_LEGACY_NOTIFICATION_PREFERENCES,
agenticCli: DEFAULT_AGENTIC_CLI_PREFERENCES,
});
});

it('does not mutate DEFAULT_AGENTIC_CLI_PREFERENCES when coercing legacy payloads', async () => {
handleMockGetNotificationPreferences({
status: 200,
body: MOCK_LEGACY_NOTIFICATION_PREFERENCES,
});
const { service } = createService();

const result = await service.getNotificationPreferences();

expect(result).not.toBeNull();
if (!result) {
throw new Error('Result is null');
}
result.agenticCli.inAppNotificationsEnabled = false;

expect(DEFAULT_AGENTIC_CLI_PREFERENCES.inAppNotificationsEnabled).toBe(
true,
);
});
});

describe('putNotificationPreferences', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
assertAssetsWatchlistBlobForWrite,
assertDelegationResponseArray,
assertNotificationPreferences,
DEFAULT_AGENTIC_CLI_PREFERENCES,
} from './validators';

// === GENERAL ===
Expand Down Expand Up @@ -271,6 +272,12 @@ export class AuthenticatedUserStorageService extends BaseDataService<
/**
* Returns the notification preferences for the authenticated user.
*
* Legacy payloads that omit `agenticCli` are coerced with
* {@link DEFAULT_AGENTIC_CLI_PREFERENCES} on read. When this method returns
* a non-`null` value, `agenticCli` is always present (backfilled), even
* though {@link NotificationPreferences} marks it optional until the next
* major release.
*
* @returns The notification preferences object, or `null` if none have been
* set (404).
*/
Expand Down Expand Up @@ -302,8 +309,14 @@ export class AuthenticatedUserStorageService extends BaseDataService<
return null;
}

assertNotificationPreferences(data);
return data;
// backfill agenticCli preferences if it is undefined
const backfilledData = {
...data,
agenticCli: data.agenticCli ?? { ...DEFAULT_AGENTIC_CLI_PREFERENCES },
};

assertNotificationPreferences(backfilledData);
return backfilledData;
}

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/authenticated-user-storage/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ export {
getAuthenticatedStorageUrl,
AuthenticatedUserStorageService,
} from './authenticated-user-storage';
export { ASSETS_WATCHLIST_MAX_ASSETS } from './validators';
export {
ASSETS_WATCHLIST_MAX_ASSETS,
DEFAULT_AGENTIC_CLI_PREFERENCES,
} from './validators';
export type {
AuthenticatedUserStorageActions,
AuthenticatedUserStorageCacheUpdatedEvent,
Expand Down Expand Up @@ -35,6 +38,7 @@ export type {
PerpsWatchlistMarkets,
PerpsPreference,
SocialAIPreference,
AgenticCliPreference,
NotificationPreferences,
AssetsWatchlistBlob,
ClientType,
Expand Down
16 changes: 15 additions & 1 deletion packages/authenticated-user-storage/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export type WalletActivityAccount = {
enabled: boolean;
};

export type AgenticCliPreference = {
inAppNotificationsEnabled: boolean;
pushNotificationsEnabled: boolean;
};

export type WalletActivityPreference = {
inAppNotificationsEnabled: boolean;
pushNotificationsEnabled: boolean;
Expand Down Expand Up @@ -103,12 +108,21 @@ export type SocialAIPreference = {
mutedTraderProfileIds: string[];
};

/** Notification preferences for the authenticated user. */
/**
* Notification preferences for the authenticated user.
*
* `agenticCli` is optional on this type for the current minor release.
* {@link AuthenticatedUserStorageService.getNotificationPreferences} always
* backfills it when absent from stored data. The next major release should
* make `agenticCli` required on this type.
*/
export type NotificationPreferences = {
walletActivity: WalletActivityPreference;
marketing: MarketingPreference;
perps: PerpsPreference;
socialAI: SocialAIPreference;
/** Optional until the next major release; always backfilled on read when absent. */
agenticCli?: AgenticCliPreference;
};

// ---------------------------------------------------------------------------
Expand Down
21 changes: 20 additions & 1 deletion packages/authenticated-user-storage/src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
type,
} from '@metamask/superstruct';

import type { DelegationResponse, NotificationPreferences } from './types';
import type {
AgenticCliPreference,
DelegationResponse,
NotificationPreferences,
} from './types';

/**
* Matches a 0x-prefixed hex string with zero or more hex digits.
Expand Down Expand Up @@ -91,13 +95,28 @@ const SocialAIPreferenceSchema = type({
mutedTraderProfileIds: array(string()),
});

const AgenticCliPreferenceSchema = type({
inAppNotificationsEnabled: boolean(),
pushNotificationsEnabled: boolean(),
});

const NotificationPreferencesSchema = type({
walletActivity: WalletActivityPreferenceSchema,
marketing: MarketingPreferenceSchema,
perps: PerpsPreferenceSchema,
socialAI: SocialAIPreferenceSchema,
agenticCli: AgenticCliPreferenceSchema,
});

/**
* Default Agentic CLI notification preferences applied when coercing legacy
* notification-preference blobs that omit `agenticCli`.
*/
export const DEFAULT_AGENTIC_CLI_PREFERENCES: AgenticCliPreference = {
inAppNotificationsEnabled: true,
pushNotificationsEnabled: true,
};

/**
* Maximum number of entries allowed in an assets-watchlist on write. Reads
* are lenient: a server payload exceeding this size will still validate as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ export const MOCK_NOTIFICATION_PREFERENCES: NotificationPreferences = {
'e8f2a1b3-5c4d-4e6f-8a9b-2c3d4e5f6a7b',
],
},
agenticCli: {
inAppNotificationsEnabled: true,
pushNotificationsEnabled: false,
},
};

/** Legacy notification preferences blob without `agenticCli`. */
export const MOCK_LEGACY_NOTIFICATION_PREFERENCES: NotificationPreferences = {
walletActivity: MOCK_NOTIFICATION_PREFERENCES.walletActivity,
marketing: MOCK_NOTIFICATION_PREFERENCES.marketing,
perps: MOCK_NOTIFICATION_PREFERENCES.perps,
socialAI: MOCK_NOTIFICATION_PREFERENCES.socialAI,
};

export const MOCK_ASSETS_WATCHLIST_BLOB: AssetsWatchlistBlob = {
Expand Down
7 changes: 7 additions & 0 deletions packages/notification-services-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `DEFAULT_AGENTIC_CLI_PREFERENCES` and initialize `agenticCli` when building fresh notification preferences via `NotificationServicesController` ([#8933](https://github.com/MetaMask/core/pull/8933))
- Re-export `DEFAULT_AGENTIC_CLI_PREFERENCES` from `@metamask/authenticated-user-storage`.

### Changed

- Agentic CLI notification delivery is gated by the Agentic backend using AUS `agenticCli` preferences; `NotificationServicesController` does not filter Agentic CLI notifications at fetch time (same as `perps` and `socialAI`) ([#8933](https://github.com/MetaMask/core/pull/8933))

- Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074))
- Bump `@metamask/controller-utils` from `^12.1.1` to `^12.2.0` ([#9083](https://github.com/MetaMask/core/pull/9083))
- Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
} from './mocks/mock-feature-announcements';
import { createMockNotificationEthSent } from './mocks/mock-raw-notifications';
import {
DEFAULT_AGENTIC_CLI_PREFERENCES,
DEFAULT_PERPS_PREFERENCES,
DEFAULT_SOCIAL_AI_PREFERENCES,
NotificationServicesController,
Expand Down Expand Up @@ -105,6 +106,7 @@ const prefsFromAddresses = (
pushNotificationsEnabled: true,
mutedTraderProfileIds: [],
},
agenticCli: { ...DEFAULT_AGENTIC_CLI_PREFERENCES },
});

const prefsFromAddressesWithMarketingInAppNotifications = (
Expand Down Expand Up @@ -565,6 +567,7 @@ describe('NotificationServicesController', () => {
},
perps: { ...DEFAULT_PERPS_PREFERENCES },
socialAI: { ...DEFAULT_SOCIAL_AI_PREFERENCES },
agenticCli: { ...DEFAULT_AGENTIC_CLI_PREFERENCES },
});
expect(mockEnablePushNotifications).toHaveBeenCalledWith([
ADDRESS_1.toLowerCase(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SocialAIPreference,
WalletActivityAccount,
} from '@metamask/authenticated-user-storage';
import { DEFAULT_AGENTIC_CLI_PREFERENCES } from '@metamask/authenticated-user-storage';
import type {
ControllerGetStateAction,
ControllerStateChangeEvent,
Expand Down Expand Up @@ -244,6 +245,8 @@ export const DEFAULT_SOCIAL_AI_PREFERENCES: Required<SocialAIPreference> = {
mutedTraderProfileIds: [],
};

export { DEFAULT_AGENTIC_CLI_PREFERENCES } from '@metamask/authenticated-user-storage';

/**
* Builds wallet-activity preferences from the keyring's current accounts.
*
Expand Down Expand Up @@ -301,8 +304,8 @@ const buildWalletActivityAccountsFromTriggerConfig = async (

/**
* 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.
* Perps, Social AI, and Agentic CLI, 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.
Expand All @@ -325,6 +328,7 @@ const buildFreshPreferences = (
},
perps: { ...DEFAULT_PERPS_PREFERENCES },
socialAI: { ...DEFAULT_SOCIAL_AI_PREFERENCES },
agenticCli: { ...DEFAULT_AGENTIC_CLI_PREFERENCES },
});

const MESSENGER_EXPOSED_METHODS = [
Expand Down
1 change: 1 addition & 0 deletions packages/notification-services-controller/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * as NotificationServicesController from './NotificationServicesController';
export * as NotificationServicesPushController from './NotificationServicesPushController';
export {
DEFAULT_AGENTIC_CLI_PREFERENCES,
DEFAULT_PERPS_PREFERENCES,
DEFAULT_SOCIAL_AI_PREFERENCES,
} from './NotificationServicesController';
Loading