Skip to content

Commit

Permalink
Merge branch 'main' into 231208-typescript-module-resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
MajorLift committed Jul 19, 2024
2 parents 59fe394 + e441444 commit 6b0075d
Show file tree
Hide file tree
Showing 22 changed files with 274 additions and 164 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/core-monorepo",
"version": "177.0.0",
"version": "178.0.0",
"private": true,
"description": "Monorepo for packages shared between MetaMask clients",
"repository": {
Expand Down
15 changes: 14 additions & 1 deletion packages/notification-services-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.2]

### 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

- removed retries when fetching announcements and wallet notifications. Clients are to handle retries now. ([#4531](https://github.com/MetaMask/core/pull/4531))

## [0.1.1]

### Added
Expand All @@ -29,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial release

[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/[email protected]
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/[email protected]
[0.1.2]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
[0.1.1]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
[0.1.0]: https://github.com/MetaMask/core/releases/tag/@metamask/[email protected]
6 changes: 3 additions & 3 deletions packages/notification-services-controller/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/notification-services-controller",
"version": "0.1.1",
"version": "0.1.2",
"description": "Manages New MetaMask decentralized Notification system",
"keywords": [
"MetaMask",
Expand Down Expand Up @@ -45,7 +45,7 @@
"@metamask/base-controller": "^6.0.1",
"@metamask/controller-utils": "^11.0.1",
"@metamask/keyring-controller": "^17.1.1",
"@metamask/profile-sync-controller": "^0.1.3",
"@metamask/profile-sync-controller": "^0.1.4",
"bignumber.js": "^4.1.0",
"contentful": "^10.3.6",
"firebase": "^10.11.0",
Expand All @@ -68,7 +68,7 @@
},
"peerDependencies": {
"@metamask/keyring-controller": "^17.0.0",
"@metamask/profile-sync-controller": "^0.1.2"
"@metamask/profile-sync-controller": "^0.1.4"
},
"engines": {
"node": "^18.18 || >=20"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,28 +284,40 @@ export default class NotificationServicesController extends BaseController<
if (!this.#isPushIntegrated) {
return;
}
await this.messagingSystem.call(
'NotificationServicesPushController:enablePushNotifications',
UUIDs,
);
try {
await this.messagingSystem.call(
'NotificationServicesPushController:enablePushNotifications',
UUIDs,
);
} catch (e) {
log.error('Silently failed to enable push notifications', e);
}
},
disablePushNotifications: async (UUIDs: string[]) => {
if (!this.#isPushIntegrated) {
return;
}
await this.messagingSystem.call(
'NotificationServicesPushController:disablePushNotifications',
UUIDs,
);
try {
await this.messagingSystem.call(
'NotificationServicesPushController:disablePushNotifications',
UUIDs,
);
} catch (e) {
log.error('Silently failed to disable push notifications', e);
}
},
updatePushNotifications: async (UUIDs: string[]) => {
if (!this.#isPushIntegrated) {
return;
}
await this.messagingSystem.call(
'NotificationServicesPushController:updateTriggerPushNotifications',
UUIDs,
);
try {
await this.messagingSystem.call(
'NotificationServicesPushController:updateTriggerPushNotifications',
UUIDs,
);
} catch (e) {
log.error('Silently failed to update push notifications', e);
}
},
subscribe: () => {
if (!this.#isPushIntegrated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export function createMockNotificationEthSent(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down Expand Up @@ -48,7 +49,8 @@ export function createMockNotificationEthReceived(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down Expand Up @@ -82,7 +84,8 @@ export function createMockNotificationERC20Sent(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down Expand Up @@ -122,7 +125,8 @@ export function createMockNotificationERC20Received(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { mockFetchFeatureAnnouncementNotifications } from '../__fixtures__/mockS
import { TRIGGER_TYPES } from '../constants/notification-schema';
import { getFeatureAnnouncementNotifications } from './feature-announcements';

// Mocked type for testing, allows overwriting TS to test erroneous values
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MockedType = any;

jest.mock('@contentful/rich-text-html-renderer', () => ({
documentToHtmlString: jest
.fn()
Expand All @@ -20,6 +24,27 @@ describe('Feature Announcement Notifications', () => {
jest.clearAllMocks();
});

it('should return an empty array if invalid environment provided', async () => {
mockFetchFeatureAnnouncementNotifications();

const assertEnvEmpty = async (
override: Partial<typeof featureAnnouncementsEnv>,
) => {
const result = await getFeatureAnnouncementNotifications({
...featureAnnouncementsEnv,
...override,
});
expect(result).toHaveLength(0);
};

await assertEnvEmpty({ accessToken: null as MockedType });
await assertEnvEmpty({ platform: null as MockedType });
await assertEnvEmpty({ spaceId: null as MockedType });
await assertEnvEmpty({ accessToken: '' });
await assertEnvEmpty({ platform: '' });
await assertEnvEmpty({ spaceId: '' });
});

it('should return an empty array if fetch fails', async () => {
const mockEndpoint = mockFetchFeatureAnnouncementNotifications({
status: 500,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import type { Entry, Asset } from 'contentful';
import log from 'loglevel';
import type { Entry, Asset, EntryCollection } from 'contentful';

import { TRIGGER_TYPES } from '../constants/notification-schema';
import { processFeatureAnnouncement } from '../processors/process-feature-announcement';
Expand Down Expand Up @@ -37,53 +36,30 @@ export type ContentfulResult = {
items?: TypeFeatureAnnouncement[];
};

const fetchFromContentful = async (
url: string,
retries = 3,
retryDelay = 1000,
): Promise<ContentfulResult | null> => {
let lastError: Error | null = null;

for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Fetch failed with status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
lastError = error;
}
if (i < retries - 1) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}

log.error(
`Error fetching from Contentful after ${retries} retries:`,
lastError,
);
return null;
};
const getFeatureAnnouncementUrl = (env: Env) =>
FEATURE_ANNOUNCEMENT_URL.replace(DEFAULT_SPACE_ID, env.spaceId)
.replace(DEFAULT_ACCESS_TOKEN, env.accessToken)
.replace(DEFAULT_CLIENT_ID, env.platform);

const fetchFeatureAnnouncementNotifications = async (
env: Env,
): Promise<FeatureAnnouncementRawNotification[]> => {
const url = FEATURE_ANNOUNCEMENT_URL.replace(DEFAULT_SPACE_ID, env.spaceId)
.replace(DEFAULT_ACCESS_TOKEN, env.accessToken)
.replace(DEFAULT_CLIENT_ID, env.platform);
const data = await fetchFromContentful(url);
const url = getFeatureAnnouncementUrl(env);

const data = await fetch(url)
.then((r) => r.json())
.catch(() => null);

if (!data) {
return [];
}

const findIncludedItem = (sysId: string) => {
const typedData: EntryCollection<ImageFields | TypeExtensionLinkFields> =
data;
const item =
data?.includes?.Entry?.find((i: Entry) => i?.sys?.id === sysId) ||
data?.includes?.Asset?.find((i: Asset) => i?.sys?.id === sysId);
typedData?.includes?.Entry?.find((i: Entry) => i?.sys?.id === sysId) ||
typedData?.includes?.Asset?.find((i: Asset) => i?.sys?.id === sysId);
return item ? item?.fields : null;
};

Expand All @@ -94,6 +70,7 @@ const fetchFeatureAnnouncementNotifications = async (
const imageFields = fields.image
? (findIncludedItem(fields.image.sys.id) as ImageFields['fields'])
: undefined;

const extensionLinkFields = fields.extensionLink
? (findIncludedItem(
fields.extensionLink.sys.id,
Expand Down Expand Up @@ -135,10 +112,14 @@ const fetchFeatureAnnouncementNotifications = async (
export async function getFeatureAnnouncementNotifications(
env: Env,
): Promise<INotification[]> {
const rawNotifications = await fetchFeatureAnnouncementNotifications(env);
const notifications = rawNotifications.map((notification) =>
processFeatureAnnouncement(notification),
);
if (env?.accessToken && env?.spaceId && env?.platform) {
const rawNotifications = await fetchFeatureAnnouncementNotifications(env);
const notifications = rawNotifications.map((notification) =>
processFeatureAnnouncement(notification),
);

return notifications;
}

return notifications;
return [];
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import log from 'loglevel';
import { v4 as uuidv4 } from 'uuid';

import {
Expand Down Expand Up @@ -425,65 +424,20 @@ export function toggleUserStorageTriggerStatus(
return userStorage;
}

/**
* Attempts to fetch a resource from the network, retrying the request up to a specified number of times
* in case of failure, with a delay between attempts.
*
* @param url - The resource URL.
* @param options - The options for the fetch request.
* @param retries - Maximum number of retry attempts. Defaults to 3.
* @param retryDelay - Delay between retry attempts in milliseconds. Defaults to 1000.
* @returns A Promise resolving to the Response object.
* @throws Will throw an error if the request fails after the specified number of retries.
*/
async function fetchWithRetry(
url: string,
options: RequestInit,
retries = 3,
retryDelay = 1000,
): Promise<Response> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Fetch failed with status: ${response.status}`);
}
return response;
} catch (error) {
log.error(`Attempt ${attempt} failed for fetch:`, error);
if (attempt < retries) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
} else {
throw new Error(
`Fetching failed after ${retries} retries. Last error: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
}
}
}

throw new Error('Unexpected error in fetchWithRetry');
}

/**
* Performs an API call with automatic retries on failure.
*
* @param bearerToken - The JSON Web Token for authorization.
* @param endpoint - The URL of the API endpoint to call.
* @param method - The HTTP method ('POST' or 'DELETE').
* @param body - The body of the request. It should be an object that can be serialized to JSON.
* @param retries - The number of retry attempts in case of failure (default is 3).
* @param retryDelay - The delay between retries in milliseconds (default is 1000).
* @returns A Promise that resolves to the response of the fetch request.
*/
export async function makeApiCall<Body>(
bearerToken: string,
endpoint: string,
method: 'POST' | 'DELETE',
body: Body,
retries = 3,
retryDelay = 1000,
): Promise<Response> {
const options: RequestInit = {
method,
Expand All @@ -494,5 +448,5 @@ export async function makeApiCall<Body>(
body: JSON.stringify(body),
};

return fetchWithRetry(endpoint, options, retries, retryDelay);
return await fetch(endpoint, options);
}
Loading

0 comments on commit 6b0075d

Please sign in to comment.