Skip to content

Commit

Permalink
fix: push notifications (#11250)
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 re-enable push notifications (background/foreground) based on
notifee approach.

## **Related issues**

Fixes:


[NOTIFY-1103](https://consensyssoftware.atlassian.net/issues/?jql=project+%3D+%22NOTIFY%22+AND+assignee+%3D+712020%3A5eed27f4-5f0b-4c08-92a0-9879b5f69a8d+ORDER+BY+created+DESC&atlOrigin=eyJpIjoiYmFjNDg1NDY1Y2ZlNDRiMGEwNTI0N2MzNzcxZjlmZjkiLCJwIjoiaiJ9)
## **Manual testing steps**

1. Install the MetaMask wallet
2. Click on the Bell Icon on the top-right header
3.Turn On Notifications
4. Trigger any transaction and leave the app.

Expected behaviour: A device push notification should appear on your
device.

## **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/59938cbf-4a2c-4409-9b68-085ee67dcc9d


<!-- [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-1103]:
https://consensyssoftware.atlassian.net/browse/NOTIFY-1103?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
Jonathansoufer authored Sep 24, 2024
1 parent a2adb20 commit dcca986
Show file tree
Hide file tree
Showing 37 changed files with 1,315 additions and 569 deletions.
30 changes: 8 additions & 22 deletions app/components/Nav/Main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,24 @@ import BackgroundTimer from 'react-native-background-timer';
import NotificationManager from '../../../core/NotificationManager';
import Engine from '../../../core/Engine';
import AppConstants from '../../../core/AppConstants';
import notifee from '@notifee/react-native';
import I18n, { strings } from '../../../../locales/i18n';
import FadeOutOverlay from '../../UI/FadeOutOverlay';
import BackupAlert from '../../UI/BackupAlert';
import Notification from '../../UI/Notification';
import RampOrders from '../../UI/Ramp';
import Device from '../../../util/device';
import Routes from '../../../constants/navigation/Routes';
import {
showTransactionNotification,
hideCurrentNotification,
showSimpleNotification,
removeNotificationById,
removeNotVisibleNotifications,
} from '../../../actions/notification';

import ProtectYourWalletModal from '../../UI/ProtectYourWalletModal';
import MainNavigator from './MainNavigator';
import SkipAccountSecurityModal from '../../UI/SkipAccountSecurityModal';
import { query } from '@metamask/controller-utils';
import SwapsLiveness from '../../UI/Swaps/SwapsLiveness';
import useNotificationHandler from '../../../util/notifications/hooks';

import {
setInfuraAvailabilityBlocked,
Expand Down Expand Up @@ -70,6 +67,8 @@ import {
selectNetworkImageSource,
} from '../../../selectors/networkInfos';
import { selectShowIncomingTransactionNetworks } from '../../../selectors/preferencesController';

import useNotificationHandler from '../../../util/notifications/hooks';
import {
DEPRECATED_NETWORKS,
NETWORKS_CHAIN_ID,
Expand Down Expand Up @@ -105,16 +104,18 @@ const Main = (props) => {
const [showDeprecatedAlert, setShowDeprecatedAlert] = useState(true);
const { colors } = useTheme();
const styles = createStyles(colors);

const backgroundMode = useRef(false);
const locale = useRef(I18n.locale);
const removeConnectionStatusListener = useRef();

const removeNotVisibleNotifications = props.removeNotVisibleNotifications;

useNotificationHandler(props.navigation);
useEnableAutomaticSecurityChecks();
useMinimumVersions();




useEffect(() => {
if (DEPRECATED_NETWORKS.includes(props.chainId)) {
setShowDeprecatedAlert(true);
Expand Down Expand Up @@ -267,23 +268,8 @@ const Main = (props) => {
initForceReload();
return;
}
});

const bootstrapAndroidInitialNotification = useCallback(async () => {
if (Device.isAndroid()) {
const initialNotification = await notifee.getInitialNotification();

if (
initialNotification?.data?.action === 'tx' &&
initialNotification.data.id
) {
NotificationManager.setTransactionToView(initialNotification.data.id);
props.navigation.navigate(Routes.TRANSACTIONS_VIEW);
}
}
}, [props.navigation]);

useNotificationHandler(bootstrapAndroidInitialNotification, props.navigation);
});

// Remove all notifications that aren't visible
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,14 @@ import Icon, {
IconSize,
} from '../../../../component-library/components/Icons/Icon';
import Routes from '../../../../constants/navigation/Routes';
import {
asyncAlert,
requestPushNotificationsPermission,
} from '../../../../util/notifications';
import NotificationsService from '../../../../util/notifications/services/NotificationService';
import { MetaMetricsEvents } from '../../../../core/Analytics';
import { useEnableNotifications } from '../../../../util/notifications/hooks/useNotifications';
import { useMetrics } from '../../../hooks/useMetrics';
import {
selectIsProfileSyncingEnabled,
selectIsMetamaskNotificationsEnabled,
} from '../../../../selectors/notifications';
import { AuthorizationStatus } from '@notifee/react-native';

interface Props {
route: {
Expand Down Expand Up @@ -65,18 +61,11 @@ const BasicFunctionalityModal = ({ route }: Props) => {
const { enableNotifications } = useEnableNotifications();

const enableNotificationsFromModal = useCallback(async () => {
const nativeNotificationStatus = await requestPushNotificationsPermission(
asyncAlert,
);

if (nativeNotificationStatus?.authorizationStatus === AuthorizationStatus.AUTHORIZED) {
/**
* Although this is an async function, we are dispatching an action (firing & forget)
* to emulate optimistic UI.
*
*/
enableNotifications();
const { permission } = await NotificationsService.getAllPermissions(false);
if (permission !== 'authorized') {
return;
}
enableNotifications();
}, [enableNotifications]);

const closeBottomSheet = async () => {
Expand Down
8 changes: 4 additions & 4 deletions app/components/UI/Notification/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import React, { useCallback, useMemo } from 'react';
import notifee from '@notifee/react-native';
import NotificationsService from '../../../../util/notifications/services/NotificationService';
import { ActivityIndicator, FlatList, FlatListProps, View } from 'react-native';
import ScrollableTabView, {
DefaultTabBar,
Expand Down Expand Up @@ -75,11 +75,11 @@ function NotificationsListItem(props: NotificationsListItemProps) {
});
}

notifee.getBadgeCount().then((count) => {
NotificationsService.getBadgeCount().then((count) => {
if (count > 0) {
notifee.setBadgeCount(count - 1);
NotificationsService.decrementBadgeCount(count - 1);
} else {
notifee.setBadgeCount(0);
NotificationsService.setBadgeCount(0);
}
});

Expand Down
90 changes: 82 additions & 8 deletions app/components/Views/Notifications/OptIn/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import OptIn from './';
import OptIn from '.';
import { RootState } from '../../../../reducers';
import { backgroundState } from '../../../../util/test/initial-root-state';
import renderWithProvider, {
DeepPartial,
} from '../../../../util/test/renderWithProvider';
import { strings } from '../../../../../locales/i18n';

const mockedDispatch = jest.fn();



const mockInitialState: DeepPartial<RootState> = {
settings: {},
engine: {
Expand All @@ -18,14 +21,12 @@ const mockInitialState: DeepPartial<RootState> = {
},
},
},
};
};

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useSelector: (fn: any) => fn(mockInitialState),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockImplementation((selector) => selector(mockInitialState)),
}));

jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
Expand All @@ -38,9 +39,82 @@ jest.mock('@react-navigation/native', () => {
};
});

jest.mock('../../../../actions/notification/helpers', () => ({
enableNotificationServices: jest.fn(),
}));

jest.mock('../../../../components/hooks/useMetrics', () => ({
useMetrics: () => ({
trackEvent: jest.fn(),
}),
}));

jest.mock('../../../../util/notifications/hooks/useNotifications', () => ({
useEnableNotifications: () => ({
enableNotifications: jest.fn(),
}),
}));

jest.mock('react-native', () => ({
Linking: {
openURL: jest.fn(),
},
}));

jest.mock('../../../../selectors/notifications', () => ({
selectIsMetamaskNotificationsEnabled: jest.fn(),
}));

jest.mock('../../../../core/Analytics', () => ({
MetaMetricsEvents: {
NOTIFICATIONS_ACTIVATED: 'notifications_activated',
},
}));

jest.mock('../../../../util/theme', () => ({
useTheme: jest.fn(),
}));

jest.mock('../../../../selectors/notifications', () => ({
selectIsProfileSyncingEnabled: jest.fn(),
}));

describe('OptIn', () => {

beforeEach(() => {
jest.resetAllMocks();
});

it('should render correctly', () => {
const { toJSON } = renderWithProvider(<OptIn />);
expect(toJSON()).toMatchSnapshot();
});

it('calls enableNotifications when the button is pressed', async () => {
const { getByText } = renderWithProvider(
<OptIn />
);

const button = getByText(strings('notifications.activation_card.cta'));
expect(button).toBeDefined();
});

it('calls navigate when the cancel button is pressed', async () => {
const { getByText } = renderWithProvider(
<OptIn />
);

const button = getByText(strings('notifications.activation_card.cancel'));
expect(button).toBeDefined();
});

it('calls trackEvent when the button is pressed', async () => {
const { getByText } = renderWithProvider(
<OptIn />
);

const button = getByText(strings('notifications.activation_card.cta'));
expect(button).toBeDefined();
});
});

20 changes: 6 additions & 14 deletions app/components/Views/Notifications/OptIn/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useNavigation } from '@react-navigation/native';

import { useMetrics } from '../../../../components/hooks/useMetrics';
import { MetaMetricsEvents } from '../../../../core/Analytics';
import { AuthorizationStatus } from '@notifee/react-native';
import Button, {
ButtonVariants,
} from '../../../../component-library/components/Buttons/Button';
Expand All @@ -18,10 +17,7 @@ import EnableNotificationsCardPlaceholder from '../../../../images/enableNotific
import { createStyles } from './styles';
import Routes from '../../../../constants/navigation/Routes';
import { useSelector } from 'react-redux';
import {
asyncAlert,
requestPushNotificationsPermission,
} from '../../../../util/notifications';
import NotificationsService from '../../../../util/notifications/services/NotificationService';
import AppConstants from '../../../../core/AppConstants';
import { RootState } from '../../../../reducers';
import { useEnableNotifications } from '../../../../util/notifications/hooks/useNotifications';
Expand Down Expand Up @@ -76,14 +72,11 @@ const OptIn = () => {
},
});
} else {
const nativeNotificationStatus = await requestPushNotificationsPermission(
asyncAlert,
);

if (
nativeNotificationStatus?.authorizationStatus ===
AuthorizationStatus.AUTHORIZED
) {
const { permission } = await NotificationsService.getAllPermissions();

if (permission !== 'authorized') {
return;
}
/**
* Although this is an async function, we are dispatching an action (firing & forget)
* to emulate optimistic UI.
Expand All @@ -103,7 +96,6 @@ const OptIn = () => {
action_type: 'activated',
is_profile_syncing_enabled: isProfileSyncingEnabled,
});
}
}, [
basicFunctionalityEnabled,
enableNotifications,
Expand Down
4 changes: 2 additions & 2 deletions app/components/Views/Notifications/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import { View } from 'react-native';
import { useSelector } from 'react-redux';
import notifee from '@notifee/react-native';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { NotificationsViewSelectorsIDs } from '../../../../e2e/selectors/NotificationsView.selectors';
import styles from './styles';
Expand Down Expand Up @@ -29,6 +28,7 @@ import {
useMarkNotificationAsRead,
} from '../../../util/notifications/hooks/useNotifications';
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import NotificationsService from '../../../util/notifications/services/NotificationService';
import ButtonIcon, {
ButtonIconSizes,
} from '../../../component-library/components/Buttons/ButtonIcon';
Expand All @@ -53,7 +53,7 @@ const NotificationsView = ({

const handleMarkAllAsRead = useCallback(() => {
markNotificationAsRead(notifications);
notifee.setBadgeCount(0);
NotificationsService.setBadgeCount(0);
trackEvent(MetaMetricsEvents.NOTIFICATIONS_MARKED_ALL_AS_READ);
}, [markNotificationAsRead, notifications, trackEvent]);

Expand Down
Loading

0 comments on commit dcca986

Please sign in to comment.