Skip to content
Merged
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
27 changes: 0 additions & 27 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1813,33 +1813,6 @@
"count": 1
}
},
"packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 2
},
"id-length": {
"count": 6
}
},
"packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 8
},
"id-denylist": {
"count": 1
},
"id-length": {
"count": 4
},
"no-param-reassign": {
"count": 2
}
},
"packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 2
}
},
"packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ import type { NormalisedAPINotification } from '../types/notification-api/notifi
import type { RawSnapNotification } from '../types/snaps';

const isOnChainNotification = (
n: RawNotificationUnion,
): n is NormalisedAPINotification =>
NOTIFICATION_API_TRIGGER_TYPES_SET.has(n.type);
notification: RawNotificationUnion,
): notification is NormalisedAPINotification =>
NOTIFICATION_API_TRIGGER_TYPES_SET.has(notification.type);

const isFeatureAnnouncement = (
n: RawNotificationUnion,
): n is FeatureAnnouncementRawNotification =>
n.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT;
notification: RawNotificationUnion,
): notification is FeatureAnnouncementRawNotification =>
notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT;

const isSnapNotification = (
n: RawNotificationUnion,
): n is RawSnapNotification => n.type === TRIGGER_TYPES.SNAP;
notification: RawNotificationUnion,
): notification is RawSnapNotification =>
notification.type === TRIGGER_TYPES.SNAP;

/**
* Process feature announcement and wallet notifications into a shared/normalised notification shape.
Expand All @@ -42,15 +43,18 @@ export function processNotification(
notification: RawNotificationUnion,
readNotifications: string[] = [],
): INotification {
const exhaustedAllCases = (_: never) => {
const exhaustedAllCases = (_uncheckedCase: never): never => {
const type: string = notification?.type;
throw new Error(`No processor found for notification kind ${type}`);
};

if (isFeatureAnnouncement(notification)) {
const n = processFeatureAnnouncement(notification);
n.isRead = isFeatureAnnouncementRead(n, readNotifications);
return n;
const processedNotification = processFeatureAnnouncement(notification);
processedNotification.isRead = isFeatureAnnouncementRead(
processedNotification,
readNotifications,
);
return processedNotification;
}

if (isSnapNotification(notification)) {
Expand Down Expand Up @@ -86,8 +90,11 @@ export function safeProcessNotification(
}
}

const isNotUndefined = <Item>(t?: Item): t is Item => Boolean(t);
const isNotUndefined = <Item>(item?: Item): item is Item => Boolean(item);
export const processAndFilterNotifications = (
ns: RawNotificationUnion[],
notifications: RawNotificationUnion[],
readIds: string[],
) => ns.map((n) => safeProcessNotification(n, readIds)).filter(isNotUndefined);
): INotification[] =>
notifications
.map((notification) => safeProcessNotification(notification, readIds))
.filter(isNotUndefined);
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const TRIGGER_API_ENV = {
prd: 'https://trigger.api.cx.metamask.io',
} satisfies Record<ENV, string>;

export const TRIGGER_API = (env: ENV = 'prd') =>
export const TRIGGER_API = (env: ENV = 'prd'): string =>
TRIGGER_API_ENV[env] ?? TRIGGER_API_ENV.prd;

const NOTIFICATION_API_ENV = {
Expand All @@ -33,24 +33,26 @@ const NOTIFICATION_API_ENV = {
prd: 'https://notification.api.cx.metamask.io',
};

export const NOTIFICATION_API = (env: ENV = 'prd') =>
export const NOTIFICATION_API = (env: ENV = 'prd'): string =>
NOTIFICATION_API_ENV[env] ?? NOTIFICATION_API_ENV.prd;

// Gets notification settings for each account provided
export const TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT = (env: ENV = 'prd') =>
`${TRIGGER_API(env)}/api/v2/notifications/query`;
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') =>
export const TRIGGER_API_NOTIFICATIONS_ENDPOINT = (env: ENV = 'prd'): string =>
`${TRIGGER_API(env)}/api/v2/notifications`;

// Lists notifications for each account provided
export const NOTIFICATION_API_LIST_ENDPOINT = (env: ENV = 'prd') =>
export const NOTIFICATION_API_LIST_ENDPOINT = (env: ENV = 'prd'): string =>
`${NOTIFICATION_API(env)}/api/v3/notifications`;

// Makrs notifications as read
export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = (env: ENV = 'prd') =>
`${NOTIFICATION_API(env)}/api/v3/notifications/mark-as-read`;
// Marks notifications as read
export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = (
env: ENV = 'prd',
): string => `${NOTIFICATION_API(env)}/api/v3/notifications/mark-as-read`;

/**
* fetches notification config (accounts enabled vs disabled)
Expand All @@ -66,31 +68,31 @@ export async function getNotificationsApiConfigCached(
bearerToken: string,
addresses: string[],
env: ENV = 'prd',
) {
): Promise<{ address: string; enabled: boolean }[]> {
if (addresses.length === 0) {
return [];
}

addresses = addresses.map((a) => a.toLowerCase());
const normalizedAddresses = addresses.map((addr) => addr.toLowerCase());

const cached = notificationsConfigCache.get(addresses);
const cached = notificationsConfigCache.get(normalizedAddresses);
if (cached) {
return cached;
}

type RequestBody = { address: string }[];
type Response = { address: string; enabled: boolean }[];
const body: RequestBody = addresses.map((address) => ({ address }));
const data = await makeApiCall(
const body: RequestBody = normalizedAddresses.map((address) => ({ address }));
const apiResponse = await makeApiCall(
bearerToken,
TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT(env),
'POST',
body,
)
.then<Response | null>((r) => (r.ok ? r.json() : null))
.then<Response | null>((response) => (response.ok ? response.json() : null))
.catch(() => null);

const result = data ?? [];
const result = apiResponse ?? [];

if (result.length > 0) {
notificationsConfigCache.set(result);
Expand All @@ -111,25 +113,25 @@ export async function updateOnChainNotifications(
bearerToken: string,
addresses: { address: string; enabled: boolean }[],
env: ENV = 'prd',
) {
): Promise<void> {
if (addresses.length === 0) {
return;
}

addresses = addresses.map((a) => {
a.address = a.address.toLowerCase();
return a;
});
const normalizedAddresses = addresses.map((item) => ({
...item,
address: item.address.toLowerCase(),
}));

type RequestBody = { address: string; enabled: boolean }[];
const body: RequestBody = addresses;
const body: RequestBody = normalizedAddresses;
await makeApiCall(
bearerToken,
TRIGGER_API_NOTIFICATIONS_ENDPOINT(env),
'POST',
body,
)
.then(() => notificationsConfigCache.set(addresses))
.then(() => notificationsConfigCache.set(normalizedAddresses))
.catch(() => null);
}

Expand Down Expand Up @@ -160,7 +162,7 @@ export async function getAPINotifications(
Schema.paths['/api/v3/notifications']['post']['responses']['200']['content']['application/json'];

const body: RequestBody = {
addresses: addresses.map((a) => a.toLowerCase()),
addresses: addresses.map((addr) => addr.toLowerCase()),
locale,
platform,
};
Expand All @@ -170,23 +172,25 @@ export async function getAPINotifications(
'POST',
body,
)
.then<APIResponse | null>((r) => (r.ok ? r.json() : null))
.then<APIResponse | null>((response) =>
response.ok ? response.json() : null,
)
.catch(() => null);

// Transform and sort notifications
const transformedNotifications = notifications
?.map((n): UnprocessedRawNotification | undefined => {
if (!n.notification_type) {
?.map((notification): UnprocessedRawNotification | undefined => {
if (!notification.notification_type) {
return undefined;
}

try {
return toRawAPINotification(n);
return toRawAPINotification(notification);
} catch {
return undefined;
}
})
.filter((n): n is NormalisedAPINotification => Boolean(n));
.filter((item): item is NormalisedAPINotification => Boolean(item));

return transformedNotifications ?? [];
}
Expand Down Expand Up @@ -223,7 +227,7 @@ export async function markNotificationsAsRead(
'POST',
body,
);
} catch (err) {
log.error('Error marking notifications as read:', err);
} catch (error) {
log.error('Error marking notifications as read:', error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('Feature Announcement Notifications', () => {

const assertEnvEmpty = async (
override: Partial<typeof featureAnnouncementsEnv>,
) => {
): Promise<void> => {
const result = await getFeatureAnnouncementNotifications({
...featureAnnouncementsEnv,
...override,
Expand Down Expand Up @@ -125,7 +125,7 @@ describe('Feature Announcement Notifications', () => {
minimumVersion: string | undefined,
maximumVersion: string | undefined,
platformVersion: string | undefined,
) => {
): Promise<INotification[]> => {
const apiResponse = createMockFeatureAnnouncementAPIResult();
if (apiResponse.items?.[0]) {
apiResponse.items[0].fields.extensionMinimumVersionNumber = undefined;
Expand Down