Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor engine helpers to handle push notifications - 2/3 #12045

Merged
merged 13 commits into from
Nov 4, 2024
63 changes: 63 additions & 0 deletions app/actions/notification/helpers/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Import necessary libraries and modules
import { signIn, signOut, enableNotificationServices, disableNotificationServices } from '.';
import Engine from '../../../core/Engine';

jest.mock('../../../core/Engine', () => ({
resetState: jest.fn(),
context: {
AuthenticationController: {
performSignIn: jest.fn(),
performSignOut: jest.fn(),
getSessionProfile: jest.fn(),
},
NotificationServicesController: {
enableMetamaskNotifications:jest.fn(),
disableNotificationServices:jest.fn(),
checkAccountsPresence: jest.fn(),
}
},
}));

describe('Notification Helpers', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('signs in successfully and obtain profile', async () => {
(Engine.context.AuthenticationController.performSignIn as jest.Mock).mockResolvedValue('valid-access-token');
(Engine.context.AuthenticationController.getSessionProfile as jest.Mock).mockResolvedValue('valid-profile');

const result = await signIn();

expect(Engine.context.AuthenticationController.performSignIn).toHaveBeenCalled();
expect(Engine.context.AuthenticationController.getSessionProfile).toHaveBeenCalled();
expect(result).toBeUndefined();
});

it('signs out successfully', async () => {
(Engine.context.AuthenticationController.performSignOut as jest.Mock).mockResolvedValue(undefined);

const result = await signOut();

expect(Engine.context.AuthenticationController.performSignOut).toHaveBeenCalled();
expect(result).toBeUndefined();
});

it('enables notification services successfully', async () => {
(Engine.context.NotificationServicesController.enableMetamaskNotifications as jest.Mock).mockResolvedValue(undefined);

const result = await enableNotificationServices();

expect(Engine.context.NotificationServicesController.enableMetamaskNotifications).toHaveBeenCalled();
expect(result).toBeUndefined();
});

it('disables notification services successfully', async () => {
(Engine.context.NotificationServicesController.disableNotificationServices as jest.Mock).mockResolvedValue(undefined);

const result = await disableNotificationServices();

expect(Engine.context.NotificationServicesController.disableNotificationServices).toHaveBeenCalled();
expect(result).toBeUndefined();
});
});
45 changes: 41 additions & 4 deletions app/actions/notification/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { getErrorMessage } from '@metamask/utils';

import { notificationsErrors } from '../constants';
import Engine from '../../../core/Engine';
import { Notification } from '../../../util/notifications';
import { Notification, mmStorage, getAllUUIDs } from '../../../util/notifications';
import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs';

export type MarkAsReadNotificationsParam = Pick<
Notification,
Expand Down Expand Up @@ -83,7 +84,7 @@ export const checkAccountsPresence = async (accounts: string[]) => {

export const deleteOnChainTriggersByAccount = async (accounts: string[]) => {
try {
const { userStorage } =
const userStorage =
await Engine.context.NotificationServicesController.deleteOnChainTriggersByAccount(
accounts,
);
Expand All @@ -92,14 +93,15 @@ export const deleteOnChainTriggersByAccount = async (accounts: string[]) => {
notificationsErrors.DELETE_ON_CHAIN_TRIGGERS_BY_ACCOUNT,
);
}
mmStorage.saveLocal('pnUserStorage', userStorage);
} catch (error) {
return getErrorMessage(error);
}
};

export const updateOnChainTriggersByAccount = async (accounts: string[]) => {
try {
const { userStorage } =
const userStorage =
await Engine.context.NotificationServicesController.updateOnChainTriggersByAccount(
accounts,
);
Expand All @@ -108,6 +110,7 @@ export const updateOnChainTriggersByAccount = async (accounts: string[]) => {
notificationsErrors.UPDATE_ON_CHAIN_TRIGGERS_BY_ACCOUNT,
);
}
mmStorage.saveLocal('pnUserStorage', userStorage);
} catch (error) {
return getErrorMessage(error);
}
Expand All @@ -117,7 +120,7 @@ export const createOnChainTriggersByAccount = async (
resetNotifications: boolean,
) => {
try {
const { userStorage } =
const userStorage =
await Engine.context.NotificationServicesController.createOnChainTriggers(
{
resetNotifications,
Expand All @@ -129,6 +132,7 @@ export const createOnChainTriggersByAccount = async (
notificationsErrors.CREATE_ON_CHAIN_TRIGGERS_BY_ACCOUNT,
);
}
mmStorage.saveLocal('pnUserStorage', userStorage);
} catch (error) {
return getErrorMessage(error);
}
Expand Down Expand Up @@ -197,3 +201,36 @@ export const performDeleteStorage = async (): Promise<string | undefined> => {
return getErrorMessage(error);
}
};
export const enablePushNotifications = async (userStorage: UserStorage, fcmToken?: string) => {
try {
const uuids = getAllUUIDs(userStorage);
await Engine.context.NotificationServicesPushController.enablePushNotifications(
uuids,
fcmToken,
);
} catch (error) {
return getErrorMessage(error);
}
};

export const disablePushNotifications = async (userStorage: UserStorage) => {
try {
const uuids = getAllUUIDs(userStorage);
await Engine.context.NotificationServicesPushController.disablePushNotifications(
uuids,
);
} catch (error) {
return getErrorMessage(error);
}
};

export const updateTriggerPushNotifications = async (userStorage: UserStorage) => {
try {
const uuids = getAllUUIDs(userStorage);
await Engine.context.NotificationServicesPushController.updateTriggerPushNotifications(
uuids,
);
} catch (error) {
return getErrorMessage(error);
}
};
40 changes: 39 additions & 1 deletion app/core/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ import {
AuthenticationController,
UserStorageController,
} from '@metamask/profile-sync-controller';
import { NotificationServicesController } from '@metamask/notification-services-controller';
import { NotificationServicesController, NotificationServicesPushController } from '@metamask/notification-services-controller';
///: END:ONLY_INCLUDE_IF
import {
getCaveatSpecifications,
Expand Down Expand Up @@ -378,6 +378,7 @@ export interface EngineState {
AuthenticationController: AuthenticationController.AuthenticationControllerState;
UserStorageController: UserStorageController.UserStorageControllerState;
NotificationServicesController: NotificationServicesController.NotificationServicesControllerState;
NotificationServicesPushController: NotificationServicesPushController.NotificationServicesPushControllerState;
///: END:ONLY_INCLUDE_IF
PermissionController: PermissionControllerState<Permissions>;
ApprovalController: ApprovalControllerState;
Expand Down Expand Up @@ -425,6 +426,7 @@ interface Controllers {
AuthenticationController: AuthenticationController.Controller;
UserStorageController: UserStorageController.Controller;
NotificationServicesController: NotificationServicesController.Controller;
NotificationServicesPushController: NotificationServicesPushController.Controller;
///: END:ONLY_INCLUDE_IF
SwapsController: SwapsController;
}
Expand Down Expand Up @@ -1282,6 +1284,9 @@ export class Engine {
'UserStorageController:getStorageKey',
'UserStorageController:performGetStorage',
'UserStorageController:performSetStorage',
'NotificationServicesPushController:enablePushNotifications',
'NotificationServicesPushController:disablePushNotifications',
'NotificationServicesPushController:updateTriggerPushNotifications',
],
allowedEvents: [
'KeyringController:unlock',
Expand All @@ -1300,6 +1305,36 @@ export class Engine {
},
},
});

const notificationServicesPushControllerMessenger =
this.controllerMessenger.getRestricted({
name: 'NotificationServicesPushController',
allowedActions: ['AuthenticationController:getBearerToken'],
allowedEvents: [],
});

const notificationServicesPushController =
new NotificationServicesPushController.Controller({
messenger: notificationServicesPushControllerMessenger,
state: initialState.NotificationServicesPushController || { fcmToken: '' },
env: {
apiKey: process.env.FIREBASE_API_KEY ?? '',
authDomain: process.env.FIREBASE_AUTH_DOMAIN ?? '',
storageBucket: process.env.FIREBASE_STORAGE_BUCKET ?? '',
projectId: process.env.FIREBASE_PROJECT_ID ?? '',
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID ?? '',
appId: process.env.FIREBASE_APP_ID ?? '',
measurementId: process.env.FIREBASE_MEASUREMENT_ID ?? '',
vapidKey: process.env.VAPID_KEY ?? '',
},
config: {
isPushEnabled: true,
platform: 'mobile',
// TODO: Implement optionability for push notification handlers (depending of the platform) on the NotificationServicesPushController.
onPushNotificationReceived: () => Promise.resolve(undefined),
onPushNotificationClicked: () => Promise.resolve(undefined),
},
});
///: END:ONLY_INCLUDE_IF

this.transactionController = new TransactionController({
Expand Down Expand Up @@ -1615,6 +1650,7 @@ export class Engine {
authenticationController,
userStorageController,
notificationServicesController,
notificationServicesPushController,
///: END:ONLY_INCLUDE_IF
accountsController,
new PPOMController({
Expand Down Expand Up @@ -2286,6 +2322,7 @@ export default {
AuthenticationController,
UserStorageController,
NotificationServicesController,
NotificationServicesPushController,
///: END:ONLY_INCLUDE_IF
PermissionController,
SelectedNetworkController,
Expand Down Expand Up @@ -2331,6 +2368,7 @@ export default {
AuthenticationController,
UserStorageController,
NotificationServicesController,
NotificationServicesPushController,
///: END:ONLY_INCLUDE_IF
PermissionController,
SelectedNetworkController,
Expand Down
4 changes: 4 additions & 0 deletions app/core/EngineService/EngineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ class EngineService {
name: 'NotificationServicesController',
key: 'NotificationServicesController:stateChange',
},
{
name: 'NotificationServicesPushController',
key: 'NotificationServicesPushController:stateChange',
},
///: END:ONLY_INCLUDE_IF
{
name: 'PermissionController',
Expand Down
19 changes: 12 additions & 7 deletions app/util/notifications/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
updateOnChainTriggersByAccount,
} from '../../../actions/notification/helpers';
import { getNotificationsList } from '../../../selectors/notifications';

import { usePushNotifications } from './usePushNotifications';
/**
* Custom hook to fetch and update the list of notifications.
* Manages loading and error states internally.
Expand Down Expand Up @@ -104,12 +104,14 @@ export function useCreateNotifications(): CreateNotificationsReturn {
export function useEnableNotifications(): EnableNotificationsReturn {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>();

const { switchPushNotifications } = usePushNotifications();
const enableNotifications = useCallback(async () => {
setLoading(true);
setError(undefined);
try {
const errorMessage = await enableNotificationServices();
const errorEnablingNotifications = await enableNotificationServices();
const errorEnablingPushNotifications = await switchPushNotifications(true);
const errorMessage = errorEnablingNotifications || errorEnablingPushNotifications;

if (errorMessage) {
setError(getErrorMessage(errorMessage));
Expand All @@ -122,7 +124,7 @@ export function useEnableNotifications(): EnableNotificationsReturn {
} finally {
setLoading(false);
}
}, []);
}, [switchPushNotifications]);

return {
enableNotifications,
Expand All @@ -139,12 +141,15 @@ export function useEnableNotifications(): EnableNotificationsReturn {
export function useDisableNotifications(): DisableNotificationsReturn {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>();

const { switchPushNotifications } = usePushNotifications();
const disableNotifications = useCallback(async () => {
setLoading(true);
setError(undefined);
try {
const errorMessage = await disableNotificationServices();
const errorDisablingNotifications = await disableNotificationServices();
const errorDisablingPushNotifications = await switchPushNotifications(false);
const errorMessage = errorDisablingNotifications || errorDisablingPushNotifications;

if (errorMessage) {
setError(getErrorMessage(errorMessage));
return errorMessage;
Expand All @@ -156,7 +161,7 @@ export function useDisableNotifications(): DisableNotificationsReturn {
} finally {
setLoading(false);
}
}, []);
}, [switchPushNotifications]);

return {
disableNotifications,
Expand Down
52 changes: 52 additions & 0 deletions app/util/notifications/hooks/usePushNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useState, useCallback } from 'react';
import { getErrorMessage } from '../../errorHandling';
import {
disablePushNotifications,
enablePushNotifications,
} from '../../../actions/notification/helpers';
import { mmStorage } from '../settings';
import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs';


export function usePushNotifications() {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const resetStates = useCallback(() => {
setLoading(false);
setError(null);
}, []);

const switchPushNotifications = useCallback(
async (state: boolean) => {
resetStates();
setLoading(true);
let errorMessage: string | undefined;

try {
const userStorage: UserStorage = mmStorage.getLocal('pnUserStorage');
if (state) {
const fcmToken = mmStorage.getLocal('metaMaskFcmToken');
errorMessage = await enablePushNotifications(userStorage, fcmToken?.data);
} else {
errorMessage = await disablePushNotifications(userStorage);
}
if (errorMessage) {
setError(getErrorMessage(errorMessage));
}
} catch (e) {
errorMessage = getErrorMessage(e);
setError(errorMessage);
} finally {
setLoading(false);
}
},
[resetStates],
);


return {
switchPushNotifications,
loading,
error,
};
}
Loading
Loading