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

Support money request push notifications #41861

Merged
merged 9 commits into from
May 13, 2024
26 changes: 17 additions & 9 deletions src/libs/Notification/PushNotification/NotificationType.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
import {ValueOf} from 'type-fest';

Check failure on line 1 in src/libs/Notification/PushNotification/NotificationType.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

All imports in the declaration are only used as types. Use `import type`
import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';

const NotificationType = {
REPORT_COMMENT: 'reportComment',
MONEY_REQUEST: 'moneyRequest',
} as const;

type NotificationDataMap = {
[NotificationType.REPORT_COMMENT]: ReportCommentNotificationData;
[NotificationType.REPORT_COMMENT]: ReportCommentPushNotificationData;
[NotificationType.MONEY_REQUEST]: ReportActionPushNotificationData;
};

type NotificationData = ReportCommentNotificationData;
type PushNotificationData = ReportCommentPushNotificationData | ReportActionPushNotificationData;

type ReportCommentNotificationData = {
type BasePushNotificationData = {
title: string;
type: typeof NotificationType.REPORT_COMMENT;
reportID: number;
reportActionID: string;
shouldScrollToLastUnread?: boolean;
roomName?: string;
type: ValueOf<typeof NotificationType>;
onyxData?: OnyxServerUpdate[];
lastUpdateID?: number;
previousUpdateID?: number;
};

type ReportActionPushNotificationData = BasePushNotificationData & {
reportID: number;
reportActionID: string;
};

type ReportCommentPushNotificationData = ReportActionPushNotificationData & {
roomName?: string;
};

/**
* See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
* types of push notifications sent by our API.
*/
export default NotificationType;
export type {NotificationDataMap, NotificationData, ReportCommentNotificationData};
export type {NotificationDataMap, PushNotificationData, ReportActionPushNotificationData, ReportCommentPushNotificationData};
6 changes: 3 additions & 3 deletions src/libs/Notification/PushNotification/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import Log from '@libs/Log';
import * as PushNotificationActions from '@userActions/PushNotification';
import ONYXKEYS from '@src/ONYXKEYS';
import ForegroundNotifications from './ForegroundNotifications';
import type {NotificationData} from './NotificationType';
import type {PushNotificationData} from './NotificationType';
import NotificationType from './NotificationType';
import type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types';
import type PushNotificationType from './types';

type NotificationEventActionCallback = (data: NotificationData) => Promise<void>;
type NotificationEventActionCallback = (data: PushNotificationData) => Promise<void>;

type NotificationEventActionMap = Partial<Record<EventType, Record<string, NotificationEventActionCallback>>>;

Expand All @@ -34,7 +34,7 @@ function pushNotificationEventCallback(eventType: EventType, notification: PushP
payload = JSON.parse(payload);
}

const data = payload as NotificationData;
const data = payload as PushNotificationData;

Log.info(`[PushNotification] Callback triggered for ${eventType}`);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {PushPayload} from '@ua/react-native-airship';
import Log from '@libs/Log';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
import * as Report from '@userActions/Report';
import type {NotificationData} from './NotificationType';
import type {PushNotificationData} from './NotificationType';

/**
* Returns whether the given Airship notification should be shown depending on the current state of the app
Expand All @@ -17,7 +17,7 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo
payload = JSON.parse(payload);
}

const data = payload as NotificationData;
const data = payload as PushNotificationData;

if (!data.reportID) {
Log.info('[PushNotification] Not a report action notification. Showing notification');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import Onyx from 'react-native-onyx';
import subscribeToReportCommentPushNotifications from '@libs/Notification/PushNotification/subscribeToReportCommentPushNotifications';
import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import {extractPolicyIDFromPath} from '@libs/PolicyUtils';
import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils';
import Visibility from '@libs/Visibility';
import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import PushNotification from '..';
import {ReportActionPushNotificationData} from '../NotificationType';

Check warning on line 17 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Unexpected parent import '../NotificationType'. Use '@libs/Notification/PushNotification/NotificationType' instead

Check failure on line 17 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

All imports in the declaration are only used as types. Use `import type`

/**
* Manage push notification subscriptions on sign-in/sign-out.
Expand All @@ -14,13 +27,109 @@
callback: (notificationID) => {
if (notificationID) {
PushNotification.register(notificationID);

// Prevent issue where report linking fails after users switch accounts without closing the app
PushNotification.init();
subscribeToReportCommentPushNotifications();

// Subscribe handlers for different push notification types
PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, applyOnyxData);

Check failure on line 33 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

'applyOnyxData' was used before it was defined
PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, navigateToReport);

Check failure on line 34 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

'navigateToReport' was used before it was defined

PushNotification.onReceived(PushNotification.TYPE.MONEY_REQUEST, applyOnyxData);

Check failure on line 36 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

'applyOnyxData' was used before it was defined
PushNotification.onSelected(PushNotification.TYPE.MONEY_REQUEST, navigateToReport);

Check failure on line 37 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

'navigateToReport' was used before it was defined
} else {
PushNotification.deregister();
PushNotification.clearNotifications();
}
},
});

let lastVisitedPath: string | undefined;
Onyx.connect({
key: ONYXKEYS.LAST_VISITED_PATH,
callback: (value) => {
if (!value) {
return;
}
lastVisitedPath = value;
},
});

function applyOnyxData({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}: ReportActionPushNotificationData): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional function comments would be useful here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but I'm not sure there's anything to comment on. "Apply the onyx data" seems like a useless comment and we didn't have any comments for it before either

Log.info(`[PushNotification] Applying onyx data in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID});

if (!ActiveClientManager.isClientTheLeader()) {
Log.info('[PushNotification] received report comment notification, but ignoring it since this is not the active client');
return Promise.resolve();
}

if (!onyxData || !lastUpdateID || !previousUpdateID) {
Log.hmmm("[PushNotification] didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0});
return Promise.resolve();
}

Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0});
const updates: OnyxUpdatesFromServer = {
type: CONST.ONYX_UPDATE_TYPES.AIRSHIP,
lastUpdateID,
previousUpdateID,
updates: [
{
eventType: 'eventType',
data: onyxData,
},
],
};

/**
* When this callback runs in the background on Android (via Headless JS), no other Onyx.connect callbacks will run. This means that
* lastUpdateIDAppliedToClient will NOT be populated in other libs. To workaround this, we manually read the value here
* and pass it as a param
*/
return getLastUpdateIDAppliedToClient().then((lastUpdateIDAppliedToClient) => applyOnyxUpdatesReliably(updates, true, lastUpdateIDAppliedToClient));

Check failure on line 87 in src/libs/Notification/PushNotification/subscribePushNotification/index.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

'getLastUpdateIDAppliedToClient' was used before it was defined
}

function navigateToReport({reportID, reportActionID}: ReportActionPushNotificationData): Promise<void> {
Log.info('[PushNotification] Navigating to report', false, {reportID, reportActionID});

const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath);
const report = getReport(reportID.toString());
const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : [];
const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID);

Navigation.isNavigationReady()
.then(Navigation.waitForProtectedRoutes)
.then(() => {
// The attachment modal remains open when navigating to the report so we need to close it
Modal.close(() => {
try {
// If a chat is visible other than the one we are trying to navigate to, then we need to navigate back
if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) {
Navigation.goBack();
}

Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID});
if (!reportBelongsToWorkspace) {
Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME});
}
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID)));
} catch (error) {
let errorMessage = String(error);
if (error instanceof Error) {
errorMessage = error.message;
}

Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage});
}
});
});

return Promise.resolve();
}

function getLastUpdateIDAppliedToClient(): Promise<number> {
return new Promise((resolve) => {
Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
callback: (value) => resolve(value ?? 0),
});
});
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {PushPayload} from '@ua/react-native-airship';
import Airship from '@ua/react-native-airship';
import Log from '@libs/Log';
import type {NotificationData} from '@libs/Notification/PushNotification/NotificationType';
import type {PushNotificationData} from '@libs/Notification/PushNotification/NotificationType';
import CONST from '@src/CONST';
import type ClearReportNotifications from './types';

Expand All @@ -10,7 +10,7 @@ const parseNotificationAndReportIDs = (pushPayload: PushPayload) => {
if (typeof payload === 'string') {
payload = JSON.parse(payload);
}
const data = payload as NotificationData;
const data = payload as PushNotificationData;
return {
notificationID: pushPayload.notificationId,
reportID: String(data.reportID),
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2263,7 +2263,7 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi
}

// Only show notifications for supported types of report actions
if (!ReportActionsUtils.isNotifiableReportAction(action)) {
if (action && !ReportActionsUtils.isNotifiableReportAction(action)) {
Log.info(`${tag} No notification because this action type is not supported`, false, {actionName: action?.actionName});
return false;
}
Expand Down
11 changes: 0 additions & 11 deletions src/setup/platformSetup/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import crashlytics from '@react-native-firebase/crashlytics';
import PushNotification from '@libs/Notification/PushNotification';
import subscribeToReportCommentPushNotifications from '@libs/Notification/PushNotification/subscribeToReportCommentPushNotifications';
import Performance from '@libs/Performance';
import CONFIG from '@src/CONFIG';

Expand All @@ -12,14 +10,5 @@ export default function () {
crashlytics().setCrashlyticsCollectionEnabled(false);
}

/*
* Register callbacks for push notifications.
* When the app is completely closed, this code will be executed by a headless JS process thanks to magic in the UrbanAirship RN library.
* However, the main App component will not be mounted in this headless context, so we must register these callbacks outside of any React lifecycle.
* Otherwise, they will not be executed when the app is completely closed, and the push notification won't update the app data.
*/
PushNotification.init();
subscribeToReportCommentPushNotifications();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This subscription was a duplicate and has been unnecessary for a while

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand. The comment says that it is necessary to be executed in Headless process. Are you saying that it is not needed anymore? We are already executing it somewhere else? If so, do you have more insights on where it is called?

App currently calls this twice. One in Setup and other one in Expensify.tsx file. But this file is also called in headLess env where Expensify.tsx is not loaded so it is necessary here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've been handling the subscription here so it's a dupe:

/**
* Manage push notification subscriptions on sign-in/sign-out.
*
* On Android, AuthScreens unmounts when the app is closed with the back button so we manage the
* push subscription when the session changes here.
*/
Onyx.connect({
key: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID,
callback: (notificationID) => {
if (notificationID) {
PushNotification.register(notificationID);
PushNotification.init();
// Subscribe handlers for different push notification types
PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, applyOnyxData);
PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, navigateToReport);
PushNotification.onReceived(PushNotification.TYPE.MONEY_REQUEST, applyOnyxData);
PushNotification.onSelected(PushNotification.TYPE.MONEY_REQUEST, navigateToReport);
} else {
PushNotification.deregister();
PushNotification.clearNotifications();
}
},
});

The callbacks should work there, but I haven't been able to build Android to test it. Can you build Android to test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm I fixed the build here. Background updates are still working well on Android:

Screenshot 2024-05-10 at 10 40 26 AM


Performance.setupPerformanceObserver();
}
Loading