From 9f743b3b0b0b6811ca619c9b6fa20d1932d440a7 Mon Sep 17 00:00:00 2001
From: dukenv0307 <dukenv0307@gmail.com>
Date: Fri, 18 Aug 2023 15:30:16 +0700
Subject: [PATCH 1/6] fix: 24174

---
 .../PushNotification/index.native.js          | 23 ++++++++++++++++++
 src/libs/actions/Session/index.js             | 24 -------------------
 2 files changed, 23 insertions(+), 24 deletions(-)

diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js
index 299af69873f9..3c868c41e6f4 100644
--- a/src/libs/Notification/PushNotification/index.native.js
+++ b/src/libs/Notification/PushNotification/index.native.js
@@ -7,6 +7,7 @@ import NotificationType from './NotificationType';
 import * as PushNotification from '../../actions/PushNotification';
 import ONYXKEYS from '../../../ONYXKEYS';
 import configureForegroundNotifications from './configureForegroundNotifications';
+import subscribeToReportCommentPushNotifications from './subscribeToReportCommentPushNotifications';
 
 let isUserOptedInToPushNotifications = false;
 Onyx.connect({
@@ -185,6 +186,28 @@ function clearNotifications() {
     Airship.push.clearNotifications();
 }
 
+/**
+ * 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) {
+            register(notificationID);
+
+            // Prevent issue where report linking fails after users switch accounts without closing the app
+            init();
+            subscribeToReportCommentPushNotifications();
+        } else {
+            deregister();
+            clearNotifications();
+        }
+    },
+});
+
 export default {
     init,
     register,
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index a538291bd054..68a827763d19 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -7,7 +7,6 @@ import ONYXKEYS from '../../../ONYXKEYS';
 import redirectToSignIn from '../SignInRedirect';
 import CONFIG from '../../../CONFIG';
 import Log from '../../Log';
-import PushNotification from '../../Notification/PushNotification';
 import Timing from '../Timing';
 import CONST from '../../../CONST';
 import Timers from '../../Timers';
@@ -18,7 +17,6 @@ import * as API from '../../API';
 import * as NetworkStore from '../../Network/NetworkStore';
 import Navigation from '../../Navigation/Navigation';
 import * as Device from '../Device';
-import subscribeToReportCommentPushNotifications from '../../Notification/PushNotification/subscribeToReportCommentPushNotifications';
 import ROUTES from '../../../ROUTES';
 import * as ErrorUtils from '../../ErrorUtils';
 import * as ReportUtils from '../../ReportUtils';
@@ -37,28 +35,6 @@ Onyx.connect({
     callback: (val) => (credentials = val || {}),
 });
 
-/**
- * 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);
-
-            // Prevent issue where report linking fails after users switch accounts without closing the app
-            PushNotification.init();
-            subscribeToReportCommentPushNotifications();
-        } else {
-            PushNotification.deregister();
-            PushNotification.clearNotifications();
-        }
-    },
-});
-
 /**
  * Clears the Onyx store and redirects user to the sign in page
  */

From 2ac25900a9e5d15b0ccbf5c733573e2665e1b01f Mon Sep 17 00:00:00 2001
From: dukenv0307 <dukenv0307@gmail.com>
Date: Sun, 20 Aug 2023 22:42:30 +0700
Subject: [PATCH 2/6] fix require cycle

---
 .../PushNotificationListener.js               | 86 ++++++++++++++++++
 .../Notification/PushNotification/index.js    |  5 -
 .../PushNotification/index.native.js          | 91 +------------------
 ...bscribeToReportCommentPushNotifications.js |  7 +-
 4 files changed, 93 insertions(+), 96 deletions(-)
 create mode 100644 src/libs/Notification/PushNotification/PushNotificationListener.js

diff --git a/src/libs/Notification/PushNotification/PushNotificationListener.js b/src/libs/Notification/PushNotification/PushNotificationListener.js
new file mode 100644
index 000000000000..4054d149607a
--- /dev/null
+++ b/src/libs/Notification/PushNotification/PushNotificationListener.js
@@ -0,0 +1,86 @@
+import {EventType} from '@ua/react-native-airship';
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
+import Log from '../../Log';
+
+const notificationEventActionMap = {};
+
+/**
+ * Handle a push notification event, and trigger and bound actions.
+ *
+ * @param {String} eventType
+ * @param {Object} notification
+ */
+function pushNotificationEventCallback(eventType, notification) {
+    const actionMap = notificationEventActionMap[eventType] || {};
+    let payload = lodashGet(notification, 'extras.payload');
+
+    // On Android, some notification payloads are sent as a JSON string rather than an object
+    if (_.isString(payload)) {
+        payload = JSON.parse(payload);
+    }
+
+    Log.info(`[PushNotification] Callback triggered for ${eventType}`);
+
+    if (!payload) {
+        Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.');
+        return;
+    }
+
+    if (!payload.type) {
+        Log.warn('[PushNotification] No type value provided in payload, not executing any callback.');
+        return;
+    }
+
+    const action = actionMap[payload.type];
+    if (!action) {
+        Log.warn('[PushNotification] No callback set up: ', {
+            event: eventType,
+            notificationType: payload.type,
+        });
+        return;
+    }
+    action(payload);
+}
+
+/**
+ * Bind a callback to a push notification of a given type.
+ * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
+ * types of push notifications sent, along with the data that they provide.
+ *
+ * Note: This implementation allows for only one callback to be bound to an Event/Type pair. For example,
+ *       if we attempt to bind two callbacks to the PushReceived event for reportComment notifications,
+ *       the second will overwrite the first.
+ *
+ * @param {String} notificationType
+ * @param {Function} callback
+ * @param {String} [triggerEvent] - The event that should trigger this callback. Should be one of UrbanAirship.EventType
+ */
+function bind(notificationType, callback, triggerEvent) {
+    if (!notificationEventActionMap[triggerEvent]) {
+        notificationEventActionMap[triggerEvent] = {};
+    }
+    notificationEventActionMap[triggerEvent][notificationType] = callback;
+}
+
+/**
+ * Bind a callback to be executed when a push notification of a given type is received.
+ *
+ * @param {String} notificationType
+ * @param {Function} callback
+ */
+function onReceived(notificationType, callback) {
+    bind(notificationType, callback, EventType.PushReceived);
+}
+
+/**
+ * Bind a callback to be executed when a push notification of a given type is tapped by the user.
+ *
+ * @param {String} notificationType
+ * @param {Function} callback
+ */
+function onSelected(notificationType, callback) {
+    bind(notificationType, callback, EventType.NotificationResponse);
+}
+
+export {pushNotificationEventCallback, onReceived, onSelected};
diff --git a/src/libs/Notification/PushNotification/index.js b/src/libs/Notification/PushNotification/index.js
index 88136ff5dc72..b8475f50d11f 100644
--- a/src/libs/Notification/PushNotification/index.js
+++ b/src/libs/Notification/PushNotification/index.js
@@ -1,12 +1,7 @@
-import NotificationType from './NotificationType';
-
 // Push notifications are only supported on mobile, so we'll just noop here
 export default {
     init: () => {},
     register: () => {},
     deregister: () => {},
-    onReceived: () => {},
-    onSelected: () => {},
-    TYPE: NotificationType,
     clearNotifications: () => {},
 };
diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js
index 3c868c41e6f4..475645bb9405 100644
--- a/src/libs/Notification/PushNotification/index.native.js
+++ b/src/libs/Notification/PushNotification/index.native.js
@@ -1,13 +1,11 @@
-import _ from 'underscore';
 import Onyx from 'react-native-onyx';
 import Airship, {EventType} from '@ua/react-native-airship';
-import lodashGet from 'lodash/get';
 import Log from '../../Log';
-import NotificationType from './NotificationType';
 import * as PushNotification from '../../actions/PushNotification';
 import ONYXKEYS from '../../../ONYXKEYS';
 import configureForegroundNotifications from './configureForegroundNotifications';
 import subscribeToReportCommentPushNotifications from './subscribeToReportCommentPushNotifications';
+import * as PushNotificationListener from './PushNotificationListener';
 
 let isUserOptedInToPushNotifications = false;
 Onyx.connect({
@@ -15,46 +13,6 @@ Onyx.connect({
     callback: (val) => (isUserOptedInToPushNotifications = val),
 });
 
-const notificationEventActionMap = {};
-
-/**
- * Handle a push notification event, and trigger and bound actions.
- *
- * @param {String} eventType
- * @param {Object} notification
- */
-function pushNotificationEventCallback(eventType, notification) {
-    const actionMap = notificationEventActionMap[eventType] || {};
-    let payload = lodashGet(notification, 'extras.payload');
-
-    // On Android, some notification payloads are sent as a JSON string rather than an object
-    if (_.isString(payload)) {
-        payload = JSON.parse(payload);
-    }
-
-    Log.info(`[PushNotification] Callback triggered for ${eventType}`);
-
-    if (!payload) {
-        Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.');
-        return;
-    }
-
-    if (!payload.type) {
-        Log.warn('[PushNotification] No type value provided in payload, not executing any callback.');
-        return;
-    }
-
-    const action = actionMap[payload.type];
-    if (!action) {
-        Log.warn('[PushNotification] No callback set up: ', {
-            event: eventType,
-            notificationType: payload.type,
-        });
-        return;
-    }
-    action(payload);
-}
-
 /**
  * Check if a user is opted-in to push notifications on this device and update the `pushNotificationsEnabled` NVP accordingly.
  */
@@ -85,13 +43,13 @@ function init() {
             PushNotification.setPushNotificationOptInStatus(true);
         }
 
-        pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload);
+        PushNotificationListener.pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload);
     });
 
     // Note: the NotificationResponse event has a nested PushReceived event,
     // so event.notification refers to the same thing as notification above ^
     Airship.addListener(EventType.NotificationResponse, (event) => {
-        pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload);
+        PushNotificationListener.pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload);
     });
 
     // Keep track of which users have enabled push notifications via an NVP.
@@ -139,46 +97,6 @@ function deregister() {
     Airship.removeAllListeners(EventType.NotificationResponse);
 }
 
-/**
- * Bind a callback to a push notification of a given type.
- * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
- * types of push notifications sent, along with the data that they provide.
- *
- * Note: This implementation allows for only one callback to be bound to an Event/Type pair. For example,
- *       if we attempt to bind two callbacks to the PushReceived event for reportComment notifications,
- *       the second will overwrite the first.
- *
- * @param {String} notificationType
- * @param {Function} callback
- * @param {String} [triggerEvent] - The event that should trigger this callback. Should be one of UrbanAirship.EventType
- */
-function bind(notificationType, callback, triggerEvent) {
-    if (!notificationEventActionMap[triggerEvent]) {
-        notificationEventActionMap[triggerEvent] = {};
-    }
-    notificationEventActionMap[triggerEvent][notificationType] = callback;
-}
-
-/**
- * Bind a callback to be executed when a push notification of a given type is received.
- *
- * @param {String} notificationType
- * @param {Function} callback
- */
-function onReceived(notificationType, callback) {
-    bind(notificationType, callback, EventType.PushReceived);
-}
-
-/**
- * Bind a callback to be executed when a push notification of a given type is tapped by the user.
- *
- * @param {String} notificationType
- * @param {Function} callback
- */
-function onSelected(notificationType, callback) {
-    bind(notificationType, callback, EventType.NotificationResponse);
-}
-
 /**
  * Clear all push notifications
  */
@@ -212,8 +130,5 @@ export default {
     init,
     register,
     deregister,
-    onReceived,
-    onSelected,
-    TYPE: NotificationType,
     clearNotifications,
 };
diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
index a36fef610a39..2316fda06904 100644
--- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
+++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
@@ -1,5 +1,6 @@
 import Onyx from 'react-native-onyx';
-import PushNotification from '.';
+import NotificationType from './NotificationType';
+import * as PushNotificationListener from './PushNotificationListener';
 import ROUTES from '../../../ROUTES';
 import Log from '../../Log';
 import Navigation from '../../Navigation/Navigation';
@@ -10,14 +11,14 @@ import backgroundRefresh from './backgroundRefresh';
  * Setup reportComment push notification callbacks.
  */
 export default function subscribeToReportCommentPushNotifications() {
-    PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => {
+    PushNotificationListener.onReceived(NotificationType.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => {
         Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID});
         Onyx.update(onyxData);
         backgroundRefresh();
     });
 
     // Open correct report when push notification is clicked
-    PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID}) => {
+    PushNotificationListener.onSelected(NotificationType.REPORT_COMMENT, ({reportID, reportActionID}) => {
         if (!reportID) {
             Log.warn('[PushNotification] This push notification has no reportID');
         }

From f26ca2d569ee7b8020cb3a4d39b2b3ae32a119f3 Mon Sep 17 00:00:00 2001
From: dukenv0307 <dukenv0307@gmail.com>
Date: Mon, 21 Aug 2023 08:11:26 +0700
Subject: [PATCH 3/6] remove test pushnotification in session test

---
 tests/actions/SessionTest.js | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/tests/actions/SessionTest.js b/tests/actions/SessionTest.js
index 506279421e7e..18db5783ed41 100644
--- a/tests/actions/SessionTest.js
+++ b/tests/actions/SessionTest.js
@@ -5,16 +5,12 @@ import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
 import ONYXKEYS from '../../src/ONYXKEYS';
 import * as TestHelper from '../utils/TestHelper';
 import CONST from '../../src/CONST';
-import PushNotification from '../../src/libs/Notification/PushNotification';
 import * as App from '../../src/libs/actions/App';
 
 // We are mocking this method so that we can later test to see if it was called and what arguments it was called with.
 // We test HttpUtils.xhr() since this means that our API command turned into a network request and isn't only queued.
 HttpUtils.xhr = jest.fn();
 
-// Mocked to ensure push notifications are subscribed/unsubscribed as the session changes
-jest.mock('../../src/libs/Notification/PushNotification');
-
 Onyx.init({
     keys: ONYXKEYS,
     registerStorageEventListener: () => {},
@@ -89,11 +85,4 @@ describe('Session', () => {
                 expect(session.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN);
             });
     });
-
-    test('Push notifications are subscribed after signing in', () => TestHelper.signInWithTestUser().then(() => expect(PushNotification.register).toBeCalled()));
-
-    test('Push notifications are unsubscribed after signing out', () =>
-        TestHelper.signInWithTestUser()
-            .then(TestHelper.signOutTestUser)
-            .then(() => expect(PushNotification.deregister).toBeCalled()));
 });

From b8925d9baa713fcd16f4f5d2bba61db59bc64b03 Mon Sep 17 00:00:00 2001
From: dukenv0307 <dukenv0307@gmail.com>
Date: Mon, 21 Aug 2023 22:07:04 +0700
Subject: [PATCH 4/6] refactor file lib notification

---
 src/Expensify.js                              |   2 +
 .../PushNotificationListener.js               |  86 --------------
 .../Notification/PushNotification/index.js    |   5 +
 .../PushNotification/index.native.js          | 112 ++++++++++++++----
 .../subscribePushNotification/index.js        |  26 ++++
 ...bscribeToReportCommentPushNotifications.js |   7 +-
 6 files changed, 123 insertions(+), 115 deletions(-)
 delete mode 100644 src/libs/Notification/PushNotification/PushNotificationListener.js
 create mode 100644 src/libs/Notification/PushNotification/subscribePushNotification/index.js

diff --git a/src/Expensify.js b/src/Expensify.js
index 41176ab779d4..d5455ca9db71 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -35,6 +35,8 @@ import DeeplinkWrapper from './components/DeeplinkWrapper';
 // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
 // eslint-disable-next-line no-unused-vars
 import UnreadIndicatorUpdater from './libs/UnreadIndicatorUpdater';
+// eslint-disable-next-line no-unused-vars
+import subscribePushNotification from './libs/Notification/PushNotification/subscribePushNotification';
 
 Onyx.registerLogger(({level, message}) => {
     if (level === 'alert') {
diff --git a/src/libs/Notification/PushNotification/PushNotificationListener.js b/src/libs/Notification/PushNotification/PushNotificationListener.js
deleted file mode 100644
index 4054d149607a..000000000000
--- a/src/libs/Notification/PushNotification/PushNotificationListener.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import {EventType} from '@ua/react-native-airship';
-import lodashGet from 'lodash/get';
-import _ from 'underscore';
-import Log from '../../Log';
-
-const notificationEventActionMap = {};
-
-/**
- * Handle a push notification event, and trigger and bound actions.
- *
- * @param {String} eventType
- * @param {Object} notification
- */
-function pushNotificationEventCallback(eventType, notification) {
-    const actionMap = notificationEventActionMap[eventType] || {};
-    let payload = lodashGet(notification, 'extras.payload');
-
-    // On Android, some notification payloads are sent as a JSON string rather than an object
-    if (_.isString(payload)) {
-        payload = JSON.parse(payload);
-    }
-
-    Log.info(`[PushNotification] Callback triggered for ${eventType}`);
-
-    if (!payload) {
-        Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.');
-        return;
-    }
-
-    if (!payload.type) {
-        Log.warn('[PushNotification] No type value provided in payload, not executing any callback.');
-        return;
-    }
-
-    const action = actionMap[payload.type];
-    if (!action) {
-        Log.warn('[PushNotification] No callback set up: ', {
-            event: eventType,
-            notificationType: payload.type,
-        });
-        return;
-    }
-    action(payload);
-}
-
-/**
- * Bind a callback to a push notification of a given type.
- * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
- * types of push notifications sent, along with the data that they provide.
- *
- * Note: This implementation allows for only one callback to be bound to an Event/Type pair. For example,
- *       if we attempt to bind two callbacks to the PushReceived event for reportComment notifications,
- *       the second will overwrite the first.
- *
- * @param {String} notificationType
- * @param {Function} callback
- * @param {String} [triggerEvent] - The event that should trigger this callback. Should be one of UrbanAirship.EventType
- */
-function bind(notificationType, callback, triggerEvent) {
-    if (!notificationEventActionMap[triggerEvent]) {
-        notificationEventActionMap[triggerEvent] = {};
-    }
-    notificationEventActionMap[triggerEvent][notificationType] = callback;
-}
-
-/**
- * Bind a callback to be executed when a push notification of a given type is received.
- *
- * @param {String} notificationType
- * @param {Function} callback
- */
-function onReceived(notificationType, callback) {
-    bind(notificationType, callback, EventType.PushReceived);
-}
-
-/**
- * Bind a callback to be executed when a push notification of a given type is tapped by the user.
- *
- * @param {String} notificationType
- * @param {Function} callback
- */
-function onSelected(notificationType, callback) {
-    bind(notificationType, callback, EventType.NotificationResponse);
-}
-
-export {pushNotificationEventCallback, onReceived, onSelected};
diff --git a/src/libs/Notification/PushNotification/index.js b/src/libs/Notification/PushNotification/index.js
index b8475f50d11f..88136ff5dc72 100644
--- a/src/libs/Notification/PushNotification/index.js
+++ b/src/libs/Notification/PushNotification/index.js
@@ -1,7 +1,12 @@
+import NotificationType from './NotificationType';
+
 // Push notifications are only supported on mobile, so we'll just noop here
 export default {
     init: () => {},
     register: () => {},
     deregister: () => {},
+    onReceived: () => {},
+    onSelected: () => {},
+    TYPE: NotificationType,
     clearNotifications: () => {},
 };
diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js
index 475645bb9405..299af69873f9 100644
--- a/src/libs/Notification/PushNotification/index.native.js
+++ b/src/libs/Notification/PushNotification/index.native.js
@@ -1,11 +1,12 @@
+import _ from 'underscore';
 import Onyx from 'react-native-onyx';
 import Airship, {EventType} from '@ua/react-native-airship';
+import lodashGet from 'lodash/get';
 import Log from '../../Log';
+import NotificationType from './NotificationType';
 import * as PushNotification from '../../actions/PushNotification';
 import ONYXKEYS from '../../../ONYXKEYS';
 import configureForegroundNotifications from './configureForegroundNotifications';
-import subscribeToReportCommentPushNotifications from './subscribeToReportCommentPushNotifications';
-import * as PushNotificationListener from './PushNotificationListener';
 
 let isUserOptedInToPushNotifications = false;
 Onyx.connect({
@@ -13,6 +14,46 @@ Onyx.connect({
     callback: (val) => (isUserOptedInToPushNotifications = val),
 });
 
+const notificationEventActionMap = {};
+
+/**
+ * Handle a push notification event, and trigger and bound actions.
+ *
+ * @param {String} eventType
+ * @param {Object} notification
+ */
+function pushNotificationEventCallback(eventType, notification) {
+    const actionMap = notificationEventActionMap[eventType] || {};
+    let payload = lodashGet(notification, 'extras.payload');
+
+    // On Android, some notification payloads are sent as a JSON string rather than an object
+    if (_.isString(payload)) {
+        payload = JSON.parse(payload);
+    }
+
+    Log.info(`[PushNotification] Callback triggered for ${eventType}`);
+
+    if (!payload) {
+        Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.');
+        return;
+    }
+
+    if (!payload.type) {
+        Log.warn('[PushNotification] No type value provided in payload, not executing any callback.');
+        return;
+    }
+
+    const action = actionMap[payload.type];
+    if (!action) {
+        Log.warn('[PushNotification] No callback set up: ', {
+            event: eventType,
+            notificationType: payload.type,
+        });
+        return;
+    }
+    action(payload);
+}
+
 /**
  * Check if a user is opted-in to push notifications on this device and update the `pushNotificationsEnabled` NVP accordingly.
  */
@@ -43,13 +84,13 @@ function init() {
             PushNotification.setPushNotificationOptInStatus(true);
         }
 
-        PushNotificationListener.pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload);
+        pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload);
     });
 
     // Note: the NotificationResponse event has a nested PushReceived event,
     // so event.notification refers to the same thing as notification above ^
     Airship.addListener(EventType.NotificationResponse, (event) => {
-        PushNotificationListener.pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload);
+        pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload);
     });
 
     // Keep track of which users have enabled push notifications via an NVP.
@@ -98,37 +139,58 @@ function deregister() {
 }
 
 /**
- * Clear all push notifications
+ * Bind a callback to a push notification of a given type.
+ * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
+ * types of push notifications sent, along with the data that they provide.
+ *
+ * Note: This implementation allows for only one callback to be bound to an Event/Type pair. For example,
+ *       if we attempt to bind two callbacks to the PushReceived event for reportComment notifications,
+ *       the second will overwrite the first.
+ *
+ * @param {String} notificationType
+ * @param {Function} callback
+ * @param {String} [triggerEvent] - The event that should trigger this callback. Should be one of UrbanAirship.EventType
  */
-function clearNotifications() {
-    Airship.push.clearNotifications();
+function bind(notificationType, callback, triggerEvent) {
+    if (!notificationEventActionMap[triggerEvent]) {
+        notificationEventActionMap[triggerEvent] = {};
+    }
+    notificationEventActionMap[triggerEvent][notificationType] = callback;
 }
 
 /**
- * Manage push notification subscriptions on sign-in/sign-out.
+ * Bind a callback to be executed when a push notification of a given type is received.
  *
- * On Android, AuthScreens unmounts when the app is closed with the back button so we manage the
- * push subscription when the session changes here.
+ * @param {String} notificationType
+ * @param {Function} callback
  */
-Onyx.connect({
-    key: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID,
-    callback: (notificationID) => {
-        if (notificationID) {
-            register(notificationID);
-
-            // Prevent issue where report linking fails after users switch accounts without closing the app
-            init();
-            subscribeToReportCommentPushNotifications();
-        } else {
-            deregister();
-            clearNotifications();
-        }
-    },
-});
+function onReceived(notificationType, callback) {
+    bind(notificationType, callback, EventType.PushReceived);
+}
+
+/**
+ * Bind a callback to be executed when a push notification of a given type is tapped by the user.
+ *
+ * @param {String} notificationType
+ * @param {Function} callback
+ */
+function onSelected(notificationType, callback) {
+    bind(notificationType, callback, EventType.NotificationResponse);
+}
+
+/**
+ * Clear all push notifications
+ */
+function clearNotifications() {
+    Airship.push.clearNotifications();
+}
 
 export default {
     init,
     register,
     deregister,
+    onReceived,
+    onSelected,
+    TYPE: NotificationType,
     clearNotifications,
 };
diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.js b/src/libs/Notification/PushNotification/subscribePushNotification/index.js
new file mode 100644
index 000000000000..45dc8d8a7ae9
--- /dev/null
+++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.js
@@ -0,0 +1,26 @@
+import Onyx from 'react-native-onyx';
+import PushNotification from '..';
+import subscribeToReportCommentPushNotifications from '../subscribeToReportCommentPushNotifications';
+import ONYXKEYS from '../../../../ONYXKEYS';
+
+/**
+ * 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);
+
+            // Prevent issue where report linking fails after users switch accounts without closing the app
+            PushNotification.init();
+            subscribeToReportCommentPushNotifications();
+        } else {
+            PushNotification.deregister();
+            PushNotification.clearNotifications();
+        }
+    },
+});
diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
index 2316fda06904..a36fef610a39 100644
--- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
+++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
@@ -1,6 +1,5 @@
 import Onyx from 'react-native-onyx';
-import NotificationType from './NotificationType';
-import * as PushNotificationListener from './PushNotificationListener';
+import PushNotification from '.';
 import ROUTES from '../../../ROUTES';
 import Log from '../../Log';
 import Navigation from '../../Navigation/Navigation';
@@ -11,14 +10,14 @@ import backgroundRefresh from './backgroundRefresh';
  * Setup reportComment push notification callbacks.
  */
 export default function subscribeToReportCommentPushNotifications() {
-    PushNotificationListener.onReceived(NotificationType.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => {
+    PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => {
         Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID});
         Onyx.update(onyxData);
         backgroundRefresh();
     });
 
     // Open correct report when push notification is clicked
-    PushNotificationListener.onSelected(NotificationType.REPORT_COMMENT, ({reportID, reportActionID}) => {
+    PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID}) => {
         if (!reportID) {
             Log.warn('[PushNotification] This push notification has no reportID');
         }

From e08d1862e9bacd62ff207b98b17921408f104ba8 Mon Sep 17 00:00:00 2001
From: dukenv0307 <dukenv0307@gmail.com>
Date: Mon, 21 Aug 2023 23:23:54 +0700
Subject: [PATCH 5/6] revert unit test

---
 tests/actions/SessionTest.js | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/tests/actions/SessionTest.js b/tests/actions/SessionTest.js
index 18db5783ed41..506279421e7e 100644
--- a/tests/actions/SessionTest.js
+++ b/tests/actions/SessionTest.js
@@ -5,12 +5,16 @@ import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
 import ONYXKEYS from '../../src/ONYXKEYS';
 import * as TestHelper from '../utils/TestHelper';
 import CONST from '../../src/CONST';
+import PushNotification from '../../src/libs/Notification/PushNotification';
 import * as App from '../../src/libs/actions/App';
 
 // We are mocking this method so that we can later test to see if it was called and what arguments it was called with.
 // We test HttpUtils.xhr() since this means that our API command turned into a network request and isn't only queued.
 HttpUtils.xhr = jest.fn();
 
+// Mocked to ensure push notifications are subscribed/unsubscribed as the session changes
+jest.mock('../../src/libs/Notification/PushNotification');
+
 Onyx.init({
     keys: ONYXKEYS,
     registerStorageEventListener: () => {},
@@ -85,4 +89,11 @@ describe('Session', () => {
                 expect(session.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN);
             });
     });
+
+    test('Push notifications are subscribed after signing in', () => TestHelper.signInWithTestUser().then(() => expect(PushNotification.register).toBeCalled()));
+
+    test('Push notifications are unsubscribed after signing out', () =>
+        TestHelper.signInWithTestUser()
+            .then(TestHelper.signOutTestUser)
+            .then(() => expect(PushNotification.deregister).toBeCalled()));
 });

From a4ec93431ff43f5c04e81833f04adbe186a33d7a Mon Sep 17 00:00:00 2001
From: dukenv0307 <dukenv0307@gmail.com>
Date: Tue, 22 Aug 2023 10:27:47 +0700
Subject: [PATCH 6/6] fix test

---
 tests/actions/SessionTest.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/tests/actions/SessionTest.js b/tests/actions/SessionTest.js
index 506279421e7e..d8bfa144e358 100644
--- a/tests/actions/SessionTest.js
+++ b/tests/actions/SessionTest.js
@@ -8,6 +8,10 @@ import CONST from '../../src/CONST';
 import PushNotification from '../../src/libs/Notification/PushNotification';
 import * as App from '../../src/libs/actions/App';
 
+// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
+// eslint-disable-next-line no-unused-vars
+import subscribePushNotification from '../../src/libs/Notification/PushNotification/subscribePushNotification';
+
 // We are mocking this method so that we can later test to see if it was called and what arguments it was called with.
 // We test HttpUtils.xhr() since this means that our API command turned into a network request and isn't only queued.
 HttpUtils.xhr = jest.fn();
