Skip to content

Support transaction push notifications #59308

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

Merged
merged 6 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions src/libs/Notification/PushNotification/NotificationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ const NotificationType = {
REPORT_ACTION: 'reportAction',
REPORT_COMMENT: 'reportComment',
MONEY_REQUEST: 'moneyRequest',
TRANSACTION: 'transaction',
} as const;

type NotificationTypes = ValueOf<typeof NotificationType>;

type NotificationDataMap = {
[NotificationType.REPORT_ACTION]: ReportActionPushNotificationData;
[NotificationType.REPORT_COMMENT]: ReportActionPushNotificationData;
[NotificationType.MONEY_REQUEST]: ReportActionPushNotificationData;
[NotificationType.TRANSACTION]: TransactionPushNotificationData;
};

type PushNotificationData = ReportActionPushNotificationData;
type PushNotificationData = ReportActionPushNotificationData | TransactionPushNotificationData;

type BasePushNotificationData = {
title: string;
Expand All @@ -30,9 +34,15 @@ type ReportActionPushNotificationData = BasePushNotificationData & {
roomName?: string;
};

type TransactionPushNotificationData = BasePushNotificationData & {
transactionID: number;
reportID: number;
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, PushNotificationData, ReportActionPushNotificationData};
export type {NotificationTypes, NotificationDataMap, PushNotificationData, ReportActionPushNotificationData, TransactionPushNotificationData};
16 changes: 9 additions & 7 deletions src/libs/Notification/PushNotification/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import Airship, {EventType} from '@ua/react-native-airship';
import Log from '@libs/Log';
import ShortcutManager from '@libs/ShortcutManager';
import ForegroundNotifications from './ForegroundNotifications';
import type {PushNotificationData} from './NotificationType';
import type {NotificationDataMap, NotificationTypes} from './NotificationType';
import NotificationType from './NotificationType';
import parsePushNotificationPayload from './parsePushNotificationPayload';
import type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types';
import type PushNotificationType from './types';

type NotificationEventActionCallback = (data: PushNotificationData) => Promise<void>;
type NotificationEventHandler<T extends NotificationTypes> = (data: NotificationDataMap[T]) => Promise<void>;

type NotificationEventActionMap = Partial<Record<EventType, Record<string, NotificationEventActionCallback>>>;
type NotificationEventHandlerMap<T extends NotificationTypes> = Partial<Record<T, NotificationEventHandler<T>>>;

type NotificationEventActionMap = Partial<Record<EventType, NotificationEventHandlerMap<NotificationTypes>>>;

const notificationEventActionMap: NotificationEventActionMap = {};

Expand Down Expand Up @@ -124,8 +126,8 @@ const deregister: Deregister = () => {
*
* @param triggerEvent - The event that should trigger this callback. Should be one of UrbanAirship.EventType
*/
function bind(notificationType: string, callback: NotificationEventActionCallback, triggerEvent: EventType) {
let actionMap = notificationEventActionMap[triggerEvent];
function bind<T extends NotificationTypes>(triggerEvent: EventType, notificationType: T, callback: NotificationEventHandler<T>) {
let actionMap = notificationEventActionMap[triggerEvent] as NotificationEventHandlerMap<T> | undefined;

if (!actionMap) {
actionMap = {};
Expand All @@ -139,14 +141,14 @@ function bind(notificationType: string, callback: NotificationEventActionCallbac
* Bind a callback to be executed when a push notification of a given type is received.
*/
const onReceived: OnReceived = (notificationType, callback) => {
bind(notificationType, callback, EventType.PushReceived);
bind(EventType.PushReceived, notificationType, callback);
};

/**
* Bind a callback to be executed when a push notification of a given type is tapped by the user.
*/
const onSelected: OnSelected = (notificationType, callback) => {
bind(notificationType, callback, EventType.NotificationResponse);
bind(EventType.NotificationResponse, notificationType, callback);
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo
Log.info('[PushNotification] push notification received', false, {pushPayload});
const data = parsePushNotificationPayload(pushPayload.extras.payload);

if (data?.reportID === undefined) {
Log.info('[PushNotification] Not a report action notification. Showing notification');
if (!data) {
return true;
}

const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null);
const shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), reportAction, true);
let shouldShow = false;
if (data.type === 'transaction') {
shouldShow = true;
} else {
const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null);
shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), reportAction, true);
}

Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`);
return shouldShow;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import PushNotification from '.';
import type {ReportActionPushNotificationData} from './NotificationType';
import type {PushNotificationData} from './NotificationType';

/**
* Manage push notification subscriptions on sign-in/sign-out.
Expand All @@ -33,6 +33,9 @@ Onyx.connect({

PushNotification.onReceived(PushNotification.TYPE.REPORT_ACTION, applyOnyxData);
PushNotification.onSelected(PushNotification.TYPE.REPORT_ACTION, navigateToReport);

PushNotification.onReceived(PushNotification.TYPE.TRANSACTION, applyOnyxData);
PushNotification.onSelected(PushNotification.TYPE.TRANSACTION, navigateToReport);
Comment on lines +37 to +38
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB: We don't have to refactor or fix this now but it's kind of weird that we do the same thing for all of these different types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I'm working on removing the moneyRequest type since it's redundant. Handling transactions will also change a bit to handle some edge cases. I'm just handling the simple case for now though.

} else {
PushNotification.deregister();
PushNotification.clearNotifications();
Expand Down Expand Up @@ -62,8 +65,8 @@ Onyx.connect({
},
});

function applyOnyxData({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID, hasPendingOnyxUpdates = false}: ReportActionPushNotificationData): Promise<void> {
Log.info(`[PushNotification] Applying onyx data in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID});
function applyOnyxData({reportID, onyxData, lastUpdateID, previousUpdateID, hasPendingOnyxUpdates = false}: PushNotificationData): Promise<void> {
Log.info(`[PushNotification] Applying onyx data in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID});

const logMissingOnyxDataInfo = (isDataMissing: boolean): boolean => {
if (isDataMissing) {
Expand Down Expand Up @@ -117,8 +120,8 @@ function applyOnyxData({reportID, reportActionID, onyxData, lastUpdateID, previo
return getLastUpdateIDAppliedToClient().then((lastUpdateIDAppliedToClient) => applyOnyxUpdatesReliably(updates, {shouldRunSync: true, clientLastUpdateID: lastUpdateIDAppliedToClient}));
}

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

const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath);

Expand All @@ -129,7 +132,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati
// When transitioning to the new experience via the singleNewDotEntry flow, the navigation
// is handled elsewhere. So we cancel here to prevent double navigation.
if (isSingleNewDotEntry) {
Log.info('[PushNotification] Not navigating because this is a singleNewDotEntry flow', false, {reportID, reportActionID});
Log.info('[PushNotification] Not navigating because this is a singleNewDotEntry flow', false, {reportID});
return;
}

Expand All @@ -142,7 +145,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati
Navigation.goBack();
}

Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID});
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB: You're sure the reportActionID isn't useful in the logs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah since we're just navigating to the report, it's not really important here.

Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID});
Navigation.navigateToReportWithPolicyCheck({reportID: String(reportID), policyIDToCheck: policyID});
updateLastVisitedPath(ROUTES.REPORT_WITH_ID.getRoute(String(reportID)));
} catch (error) {
Expand All @@ -151,7 +154,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati
errorMessage = error.message;
}

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