Skip to content

Commit

Permalink
chore: notifications UI update to use FCM services (#12214)
Browse files Browse the repository at this point in the history
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR implements support to FCM (Firebase Cloud Messaging) on both iOS
and Android on mobile.

## **Related issues**

Fixes:
[NOTIFY-1219](https://consensyssoftware.atlassian.net/browse/NOTIFY-1219?atlOrigin=eyJpIjoiZDJiMzczYTc5ZGI5NGU3OWE4YTNjZDAyN2M0ZjIzYWYiLCJwIjoiaiJ9)

## **Manual testing steps**

1. Go to Settings --> Notifications --> Enable
2. Open a separated device (or extension) repeat the process above
3. Do any transaction between the two clients/devices
4. Leave the transaction recipient open to see it in Foreground
5. Put app in background to see a native notification

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**


https://github.com/user-attachments/assets/3bb403cb-c1b1-4664-bad9-95fd185d3ecc


<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


[NOTIFY-1219]:
https://consensyssoftware.atlassian.net/browse/NOTIFY-1219?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
Jonathansoufer authored Nov 7, 2024
1 parent 9b5a1e5 commit 0b5af0d
Show file tree
Hide file tree
Showing 12 changed files with 2,124 additions and 197 deletions.

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions app/components/UI/Notification/BaseNotification/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const getIcon = (status, colors, styles) => {
case 'success':
case 'received':
case 'received_payment':
case 'eth_received':
return (
<IonicIcon
color={colors.success.default}
Expand Down Expand Up @@ -147,12 +148,14 @@ const getTitle = (status, { nonce, amount, assetType }) => {
return strings('notifications.cancelled_title');
case 'error':
return strings('notifications.error_title');
case 'eth_received':
return strings('notifications.eth_received_title');
}
};

const getDescription = (status, { amount = null }) => {
if (amount) {
return strings(`notifications.${status}_message`, { amount });
export const getDescription = (status, { amount = null, type = null }) => {
if (amount && typeof amount !== 'object') {
return strings(`notifications.${type}_${status}_message`, { amount });
}
return strings(`notifications.${status}_message`);
};
Expand Down
47 changes: 47 additions & 0 deletions app/components/UI/Notification/BaseNotification/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import BaseNotification, { getDescription } from './';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import { strings } from '../../../../../locales/i18n';

const defaultProps = [
{ status: 'pending', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'pending_withdrawal', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'pending_deposit', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'success_deposit', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'success_withdrawal', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'received', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'received_payment', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'eth_received', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'cancelled', data: { description: 'Testing description', title: 'Testing Title' } },
{ status: 'error', data: { description: 'Testing description', title: 'Testing Title' } },
];

describe('BaseNotification', () => {
it('gets icon correctly for each status', () => {
defaultProps.forEach(({ status, data}) => {
const { toJSON } = renderWithProvider(<BaseNotification status={status} data={data} />);
expect(toJSON()).toMatchSnapshot();
});
});

it('gets titles correctly for each status', () => {
defaultProps.forEach(({ status }) => {
const { getByText } = renderWithProvider(<BaseNotification status={status} data={{}} />);
expect(getByText(strings(`notifications.${status}_title`))).toBeTruthy();
});
});

it('gets descriptions correctly for if they are provided', () => {
defaultProps.forEach(({ status, data }) => {
const { getByText } = renderWithProvider(<BaseNotification status={status} data={data} />);
expect(getByText(data.description)).toBeTruthy();
});
});

it('constructs the correct description using getDescription when no description is provided', () => {
defaultProps.forEach(({ status }) => {
const { getByText } = renderWithProvider(<BaseNotification status={status} data={{}} />);
expect(getByText(getDescription(status, {}))).toBeTruthy();
});
});
});
52 changes: 22 additions & 30 deletions app/core/NotificationManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@
import Engine from './Engine';
import { hexToBN, renderFromWei } from '../util/number';
import Device from '../util/device';
import notifee from '@notifee/react-native';
import { STORAGE_IDS } from '../util/notifications/settings/storage/constants';
import { strings } from '../../locales/i18n';
import { AppState } from 'react-native';
import NotificationsService from '../util/notifications/services/NotificationService';

import {
NotificationTransactionTypes,
isNotificationsFeatureEnabled,

ChannelId,
} from '../util/notifications';

import { safeToChecksumAddress } from '../util/address';
import { safeToChecksumAddress, formatAddress } from '../util/address';
import ReviewManager from './ReviewManager';
import { selectChainId, selectTicker } from '../selectors/networkController';
import { store } from '../store';
Expand Down Expand Up @@ -76,9 +74,17 @@ export const constructTitleAndMessage = (notification) => {
amount: notification.transaction.amount,
});
break;
case NotificationTransactionTypes.eth_received:
title = strings('notifications.default_message_title');
message = strings('notifications.eth_received_message', {
amount: notification.transaction.amount.usd,
ticker: 'USD',
address: formatAddress(notification.transaction.from, 'short'),
});
break;
default:
title = notification.data.title || strings('notifications.default_message_title');
message = notification.data.shortDescription || strings('notifications.default_message_description');
title = notification?.data?.title || strings('notifications.default_message_title');
message = notification?.data?.shortDescription || strings('notifications.default_message_description');
break;
}
return { title, message };
Expand Down Expand Up @@ -141,8 +147,7 @@ class NotificationManager {
);
};

// TODO: Refactor this method to use notifee's channels in combination with MM auth
_showNotification(data, channelId = STORAGE_IDS.ANDROID_DEFAULT_CHANNEL_ID) {
_showNotification = async (data) => {
if (this._backgroundMode) {
const { title, message } = constructTitleAndMessage(data);
const id = data?.transaction?.id || '';
Expand All @@ -151,26 +156,13 @@ class NotificationManager {
}

const pushData = {
channelId: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
title,
body: message,
android: {
lightUpScreen: true,
channelId,
smallIcon: 'ic_notification_small',
largeIcon: 'ic_notification',
pressAction: {
id: 'default',
launchActivity: 'com.metamask.ui.MainActivity',
},
},
ios: {
foregroundPresentationOptions: {
alert: true,
sound: true,
badge: true,
banner: true,
list: true,
},
data: {
...data?.transaction,
action: 'tx',
id,
},
};

Expand All @@ -179,17 +171,17 @@ class NotificationManager {
if (Device.isAndroid()) {
pushData.tag = JSON.stringify(extraData);
} else {
pushData.userInfo = extraData; // check if is still needed
pushData.userInfo = extraData;
}
isNotificationsFeatureEnabled() && notifee.displayNotification(pushData);
await NotificationsService.displayNotification(pushData);
} else {
this._showTransactionNotification({
autodismiss: data.duration,
transaction: data.transaction,
status: data.type,
});
}
}
};

_failedCallback = (transactionMeta) => {
// If it fails we hide the pending tx notification
Expand Down
15 changes: 14 additions & 1 deletion app/core/NotificationsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ interface NavigationMock {
navigate: jest.Mock;
}

jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));

jest.unmock('./NotificationManager');

const mockNavigate: jest.Mock = jest.fn();
Expand Down Expand Up @@ -101,13 +105,22 @@ describe('NotificationManager', () => {
'cancelled',
];
selectedNotificationTypes.forEach((type) => {
it(`should construct title and message for ${type}`, () => {
it(`constructs title and message for ${type}`, () => {
const { title, message } = constructTitleAndMessage({
type: NotificationTransactionTypes[type],
});

expect(title).toBe(strings(`notifications.${type}_title`));
expect(message).toBe(strings(`notifications.${type}_message`));
});

it('constructs default title and message for unknown type', () => {
const { title, message } = constructTitleAndMessage({
type: 'unknown',
});

expect(title).toBe(strings('notifications.default_message_title'));
expect(message).toBe(strings('notifications.default_message_description'));
});
});
});
109 changes: 0 additions & 109 deletions app/util/notifications/hooks/index.test.ts

This file was deleted.

Loading

0 comments on commit 0b5af0d

Please sign in to comment.