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

fix: push notifications #11250

Merged
merged 64 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
166c7b1
refactor push notifications listeners
Sep 17, 2024
e00650f
remove unused types
Sep 17, 2024
71c3b49
remove duplicate call
Sep 17, 2024
b6414bb
bump notifee version
Sep 17, 2024
6a40136
add feature flag
Sep 17, 2024
e14a04a
add feature flag
Sep 17, 2024
92fe808
improve debug logs & avaoid useEffect dependency lint error
Sep 17, 2024
df1b4fa
bump packages versions
Sep 17, 2024
2556fdc
refactor tests
Sep 17, 2024
5aeff10
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 17, 2024
5628710
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 18, 2024
cfd075f
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 18, 2024
dcde253
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 18, 2024
c8a802b
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 18, 2024
21b796c
update pods
Sep 18, 2024
e77ca6e
Merge branch 'main' into fix/push-notifications
Sep 19, 2024
28735ec
remove console + add deps
Sep 19, 2024
61f61b9
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 19, 2024
84b3871
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 19, 2024
f376beb
remove old approach
Sep 20, 2024
66c2ee8
create a service to split responsibilities
Sep 20, 2024
be5c74a
refactor usage
Sep 20, 2024
8d4a191
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 20, 2024
bbc3e5c
fix allow
Sep 20, 2024
012a91f
fix lint
Sep 20, 2024
9d560d7
fix lint
Sep 20, 2024
7ebb300
add e2e step
Sep 20, 2024
c39141d
fix mistankly change
Sep 20, 2024
3dea73f
bump versions
Sep 20, 2024
58f3990
revokechange
Sep 20, 2024
1ac0bff
fix types
Sep 20, 2024
4bf001b
fix lint
Sep 20, 2024
6a947aa
fix dedup
Sep 20, 2024
ca57564
fix merge conflicts
Sep 20, 2024
179169f
fix merge conflicts
Sep 20, 2024
2f256ce
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 21, 2024
1c9a055
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 23, 2024
70102dd
adapt e2e tests
Sep 23, 2024
9893026
adapt e2e tests
Sep 23, 2024
c31bc92
remove unnecessary check
Sep 23, 2024
8ad6836
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 23, 2024
9ea5394
fix permission ordere
Sep 23, 2024
c1d928f
Merge branch 'fix/push-notifications' of github.com:MetaMask/metamask…
Sep 23, 2024
e2621c3
add unit tests
Sep 23, 2024
f838e25
workaround on iOS system dialog
Sep 23, 2024
60f8d06
unit test
Sep 23, 2024
284ed4d
update tests to handle tap on system dialogs
Sep 23, 2024
cba1943
adapt method
Sep 23, 2024
8702d43
clean up notifee references
Sep 23, 2024
9e64951
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 23, 2024
ee30ae6
rollback e2e changes
Sep 23, 2024
daddbc4
remove check
Sep 23, 2024
958dc87
add unit test
Sep 23, 2024
726f1d0
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 23, 2024
6438678
add unit test
Sep 23, 2024
0bf5946
add unit test for NotificationManager
Sep 23, 2024
cc84acd
increase testing covergae
Sep 23, 2024
d063222
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 24, 2024
48f64be
remove whitespace
Sep 24, 2024
7c10b76
refactor hook and unit test
Sep 24, 2024
c4b7094
small adjusts and usage refactor
Sep 24, 2024
de718b0
rollback changes
Sep 24, 2024
d762b5e
fix lint
Sep 24, 2024
e4d7921
Merge branch 'main' into fix/push-notifications
Jonathansoufer Sep 24, 2024
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
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', () => ({
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
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
Loading