From da25fc57dfc1391a2fa043184bdcfb476ef9746c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 14 Nov 2023 06:27:31 -0300 Subject: [PATCH 01/11] Rename files to TS --- .../{index.android.js => index.android.ts} | 0 .../ForegroundNotifications/{index.ios.js => index.ios.ts} | 0 .../ForegroundNotifications/{index.js => index.ts} | 0 .../PushNotification/{NotificationType.js => NotificationType.ts} | 0 .../backgroundRefresh/{index.android.js => index.android.ts} | 0 .../PushNotification/backgroundRefresh/{index.js => index.ts} | 0 .../PushNotification/{index.native.js => index.native.ts} | 0 src/libs/Notification/PushNotification/{index.js => index.ts} | 0 ...houldShowPushNotification.js => shouldShowPushNotification.ts} | 0 .../subscribePushNotification/{index.js => index.ts} | 0 ...ifications.js => subscribeToReportCommentPushNotifications.ts} | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename src/libs/Notification/PushNotification/ForegroundNotifications/{index.android.js => index.android.ts} (100%) rename src/libs/Notification/PushNotification/ForegroundNotifications/{index.ios.js => index.ios.ts} (100%) rename src/libs/Notification/PushNotification/ForegroundNotifications/{index.js => index.ts} (100%) rename src/libs/Notification/PushNotification/{NotificationType.js => NotificationType.ts} (100%) rename src/libs/Notification/PushNotification/backgroundRefresh/{index.android.js => index.android.ts} (100%) rename src/libs/Notification/PushNotification/backgroundRefresh/{index.js => index.ts} (100%) rename src/libs/Notification/PushNotification/{index.native.js => index.native.ts} (100%) rename src/libs/Notification/PushNotification/{index.js => index.ts} (100%) rename src/libs/Notification/PushNotification/{shouldShowPushNotification.js => shouldShowPushNotification.ts} (100%) rename src/libs/Notification/PushNotification/subscribePushNotification/{index.js => index.ts} (100%) rename src/libs/Notification/PushNotification/{subscribeToReportCommentPushNotifications.js => subscribeToReportCommentPushNotifications.ts} (100%) diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts similarity index 100% rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts similarity index 100% rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts similarity index 100% rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.js rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ts diff --git a/src/libs/Notification/PushNotification/NotificationType.js b/src/libs/Notification/PushNotification/NotificationType.ts similarity index 100% rename from src/libs/Notification/PushNotification/NotificationType.js rename to src/libs/Notification/PushNotification/NotificationType.ts diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.js b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts similarity index 100% rename from src/libs/Notification/PushNotification/backgroundRefresh/index.android.js rename to src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.js b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts similarity index 100% rename from src/libs/Notification/PushNotification/backgroundRefresh/index.js rename to src/libs/Notification/PushNotification/backgroundRefresh/index.ts diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.ts similarity index 100% rename from src/libs/Notification/PushNotification/index.native.js rename to src/libs/Notification/PushNotification/index.native.ts diff --git a/src/libs/Notification/PushNotification/index.js b/src/libs/Notification/PushNotification/index.ts similarity index 100% rename from src/libs/Notification/PushNotification/index.js rename to src/libs/Notification/PushNotification/index.ts diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.js b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts similarity index 100% rename from src/libs/Notification/PushNotification/shouldShowPushNotification.js rename to src/libs/Notification/PushNotification/shouldShowPushNotification.ts diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.js b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts similarity index 100% rename from src/libs/Notification/PushNotification/subscribePushNotification/index.js rename to src/libs/Notification/PushNotification/subscribePushNotification/index.ts diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts similarity index 100% rename from src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js rename to src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts From fe710694a9e5701678886e0da54832e2266a7d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 15 Nov 2023 10:14:45 -0300 Subject: [PATCH 02/11] Migrate ForegroundNotifications to TS --- .../ForegroundNotifications/index.android.ts | 5 ++++- .../PushNotification/ForegroundNotifications/index.ios.ts | 5 ++++- .../PushNotification/ForegroundNotifications/index.ts | 6 +++++- .../PushNotification/ForegroundNotifications/types.ts | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/libs/Notification/PushNotification/ForegroundNotifications/types.ts diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts index b36c0d0c7d18..b19e1210e747 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts @@ -1,5 +1,6 @@ import Airship from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; +import ForegroundNotifications from './types'; function configureForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload))); @@ -9,7 +10,9 @@ function disableForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate(() => Promise.resolve(false)); } -export default { +const foregroundNotifications: ForegroundNotifications = { configureForegroundNotifications, disableForegroundNotifications, }; + +export default foregroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts index 0f0929951a90..2f66d08e63e0 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts @@ -1,5 +1,6 @@ import Airship, {iOS} from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; +import ForegroundNotifications from './types'; function configureForegroundNotifications() { // Set our default iOS foreground presentation to be loud with a banner @@ -20,7 +21,9 @@ function disableForegroundNotifications() { Airship.push.iOS.setForegroundPresentationOptionsCallback(() => Promise.resolve([])); } -export default { +const foregroundNotifications: ForegroundNotifications = { configureForegroundNotifications, disableForegroundNotifications, }; + +export default foregroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts index acb116f7bc43..ca390f524daf 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts @@ -1,7 +1,11 @@ +import ForegroundNotifications from './types'; + /** * Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms. */ -export default { +const foregroundNotifications: ForegroundNotifications = { configureForegroundNotifications: () => {}, disableForegroundNotifications: () => {}, }; + +export default foregroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts new file mode 100644 index 000000000000..ebceed1e4543 --- /dev/null +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts @@ -0,0 +1,6 @@ +type ForegroundNotifications = { + configureForegroundNotifications: () => void; + disableForegroundNotifications: () => void; +}; + +export default ForegroundNotifications; From 0b0f19c2c5baebd4c81202009047a532e2f05114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 15 Nov 2023 10:31:44 -0300 Subject: [PATCH 03/11] Migrate NotificationType and shouldShowPushNotification to TS --- .../PushNotification/NotificationType.ts | 2 +- .../shouldShowPushNotification.ts | 20 ++++++++++++------- src/libs/actions/Report.js | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts index 092a48fe7815..8e53af950881 100644 --- a/src/libs/Notification/PushNotification/NotificationType.ts +++ b/src/libs/Notification/PushNotification/NotificationType.ts @@ -4,4 +4,4 @@ */ export default { REPORT_COMMENT: 'reportComment', -}; +} as const; diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index f25d452a77d5..ce1f013be90f 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,23 +1,29 @@ -import _ from 'underscore'; +import {PushPayload} from '@ua/react-native-airship'; +import {OnyxUpdate} from 'react-native-onyx'; import Log from '@libs/Log'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; +type PushData = { + onyxData: OnyxUpdate[]; + reportID?: number; +}; + /** * Returns whether the given Airship notification should be shown depending on the current state of the app - * @param {PushPayload} pushPayload - * @returns {Boolean} */ -export default function shouldShowPushNotification(pushPayload) { +export default function shouldShowPushNotification(pushPayload: PushPayload): boolean { Log.info('[PushNotification] push notification received', false, {pushPayload}); - let pushData = pushPayload.extras.payload; + let payload = pushPayload.extras.payload; // The payload is string encoded on Android - if (_.isString(pushData)) { - pushData = JSON.parse(pushData); + if (typeof payload === 'string') { + payload = JSON.parse(payload); } + const pushData = payload as PushData; + if (!pushData.reportID) { Log.info('[PushNotification] Not a report action notification. Showing notification'); return true; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 22a1bc5441e6..f34f31a21511 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1753,7 +1753,7 @@ function setIsComposerFullSize(reportID, isComposerFullSize) { /** * @param {String} reportID - * @param {Object} action the associated report action (optional) + * @param {Object|null} action the associated report action (optional) * @param {Boolean} isRemote whether or not this notification is a remote push notification * @returns {Boolean} */ From 31af9ded14f7a8f877016adaf90e3dde9710be89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 28 Nov 2023 10:52:09 -0300 Subject: [PATCH 04/11] Minor fixes in backgroundRefresh --- .../backgroundRefresh/index.android.ts | 11 +++++++---- .../PushNotification/backgroundRefresh/index.ts | 6 +++++- .../PushNotification/backgroundRefresh/types.ts | 3 +++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/libs/Notification/PushNotification/backgroundRefresh/types.ts diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts index 4502011b459e..2b3c6ebf21b4 100644 --- a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts +++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts @@ -4,8 +4,9 @@ import Visibility from '@libs/Visibility'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import BackgroundRefresh from './types'; -function getLastOnyxUpdateID() { +function getLastOnyxUpdateID(): Promise { return new Promise((resolve) => { const connectionID = Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, @@ -23,7 +24,7 @@ function getLastOnyxUpdateID() { * We use this to refresh the app in the background after receiving a push notification (native only). Since the full app * wakes on iOS and by extension runs reconnectApp already, this is a no-op on everything but Android. */ -export default function backgroundRefresh() { +const backgroundRefresh: BackgroundRefresh = () => { if (Visibility.isVisible()) { return; } @@ -38,9 +39,11 @@ export default function backgroundRefresh() { * See more here: https://reactnative.dev/docs/headless-js-android */ App.confirmReadyToOpenApp(); - App.reconnectApp(lastUpdateIDAppliedToClient); + App.reconnectApp(lastUpdateIDAppliedToClient ?? undefined); }) .catch((error) => { Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} [PushNotification] backgroundRefresh failed. This should never happen.`, {error}); }); -} +}; + +export default backgroundRefresh; diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.ts b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts index 657fb15ee429..c7f47a532d89 100644 --- a/src/libs/Notification/PushNotification/backgroundRefresh/index.ts +++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts @@ -1,7 +1,11 @@ +import BackgroundRefresh from './types'; + /** * Runs our reconnectApp action if the app is in the background. * * We use this to refresh the app in the background after receiving a push notification (native only). Since the full app * wakes on iOS and by extension runs reconnectApp already, this is a no-op on everything but Android. */ -export default function backgroundRefresh() {} +const backgroundRefresh: BackgroundRefresh = () => {}; + +export default backgroundRefresh; diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/types.ts b/src/libs/Notification/PushNotification/backgroundRefresh/types.ts new file mode 100644 index 000000000000..d3d1ee44a1fd --- /dev/null +++ b/src/libs/Notification/PushNotification/backgroundRefresh/types.ts @@ -0,0 +1,3 @@ +type BackgroundRefresh = () => void; + +export default BackgroundRefresh; From 7067417cfd09eeae608181e7d9f49b3e6e72775c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 28 Nov 2023 11:08:09 -0300 Subject: [PATCH 05/11] make ForegroundNotifications capitalized --- .../ForegroundNotifications/index.android.ts | 6 +++--- .../PushNotification/ForegroundNotifications/index.ios.ts | 6 +++--- .../PushNotification/ForegroundNotifications/index.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts index b19e1210e747..8c63d81093a6 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts @@ -1,6 +1,6 @@ import Airship from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; -import ForegroundNotifications from './types'; +import ForegroundNotificationsType from './types'; function configureForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload))); @@ -10,9 +10,9 @@ function disableForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate(() => Promise.resolve(false)); } -const foregroundNotifications: ForegroundNotifications = { +const ForegroundNotifications: ForegroundNotificationsType = { configureForegroundNotifications, disableForegroundNotifications, }; -export default foregroundNotifications; +export default ForegroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts index 2f66d08e63e0..588a24a27651 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts @@ -1,6 +1,6 @@ import Airship, {iOS} from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; -import ForegroundNotifications from './types'; +import ForegroundNotificationsType from './types'; function configureForegroundNotifications() { // Set our default iOS foreground presentation to be loud with a banner @@ -21,9 +21,9 @@ function disableForegroundNotifications() { Airship.push.iOS.setForegroundPresentationOptionsCallback(() => Promise.resolve([])); } -const foregroundNotifications: ForegroundNotifications = { +const ForegroundNotifications: ForegroundNotificationsType = { configureForegroundNotifications, disableForegroundNotifications, }; -export default foregroundNotifications; +export default ForegroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts index ca390f524daf..91a68fbc3503 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts @@ -1,11 +1,11 @@ -import ForegroundNotifications from './types'; +import ForegroundNotificationsType from './types'; /** * Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms. */ -const foregroundNotifications: ForegroundNotifications = { +const ForegroundNotifications: ForegroundNotificationsType = { configureForegroundNotifications: () => {}, disableForegroundNotifications: () => {}, }; -export default foregroundNotifications; +export default ForegroundNotifications; From c8f48adac5389c719bdd95b364e1a93e92877268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 29 Nov 2023 09:56:34 -0300 Subject: [PATCH 06/11] Several improvements and PushNotification migration --- .../PushNotification/NotificationType.ts | 27 +++++- .../PushNotification/index.native.ts | 95 +++++++++---------- .../Notification/PushNotification/index.ts | 5 +- .../shouldShowPushNotification.ts | 15 +-- ...bscribeToReportCommentPushNotifications.ts | 11 ++- .../Notification/PushNotification/types.ts | 22 +++++ src/types/onyx/OnyxUpdatesFromServer.ts | 6 +- 7 files changed, 113 insertions(+), 68 deletions(-) create mode 100644 src/libs/Notification/PushNotification/types.ts diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts index 8e53af950881..a69525c96581 100644 --- a/src/libs/Notification/PushNotification/NotificationType.ts +++ b/src/libs/Notification/PushNotification/NotificationType.ts @@ -1,7 +1,28 @@ +import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; + +const NotificationType = { + REPORT_COMMENT: 'reportComment', +} as const; + +type NotificationDataMap = { + [NotificationType.REPORT_COMMENT]: ReportCommentNotificationData; +}; + +type NotificationData = ReportCommentNotificationData; + +type ReportCommentNotificationData = { + title?: string; + type?: typeof NotificationType.REPORT_COMMENT; + reportID?: number; + reportActionID?: string; + shouldScrollToLastUnread?: boolean; + roomName?: string; + onyxData?: OnyxServerUpdate[]; +}; + /** * 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 { - REPORT_COMMENT: 'reportComment', -} as const; +export default NotificationType; +export type {NotificationDataMap, NotificationData, ReportCommentNotificationData}; diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 8513a88e46d3..537989116021 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -1,57 +1,57 @@ -import Airship, {EventType} from '@ua/react-native-airship'; -import lodashGet from 'lodash/get'; +import Airship, {EventType, PushPayload} from '@ua/react-native-airship'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; import Log from '@libs/Log'; -import * as PushNotification from '@userActions/PushNotification'; +import * as PushNotificationActions from '@userActions/PushNotification'; import ONYXKEYS from '@src/ONYXKEYS'; import ForegroundNotifications from './ForegroundNotifications'; -import NotificationType from './NotificationType'; +import NotificationType, {NotificationData} from './NotificationType'; +import PushNotificationType, {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types'; + +type NotificationEventActionCallback = (data: NotificationData) => void; let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => (isUserOptedInToPushNotifications = val), + callback: (val) => (isUserOptedInToPushNotifications = val ?? false), }); -const notificationEventActionMap = {}; +const notificationEventActionMap: Partial>> = {}; /** * 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'); +function pushNotificationEventCallback(eventType: EventType, notification: PushPayload) { + const actionMap = notificationEventActionMap[eventType] ?? {}; + let payload = notification.extras.payload; // On Android, some notification payloads are sent as a JSON string rather than an object - if (_.isString(payload)) { + if (typeof payload === 'string') { payload = JSON.parse(payload); } + const data = payload as NotificationData; + Log.info(`[PushNotification] Callback triggered for ${eventType}`); - if (!payload) { + if (!data) { Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.'); return; } - if (!payload.type) { + if (!data.type) { Log.warn('[PushNotification] No type value provided in payload, not executing any callback.'); return; } - const action = actionMap[payload.type]; + const action = actionMap[data.type]; if (!action) { Log.warn('[PushNotification] No callback set up: ', { event: eventType, - notificationType: payload.type, + notificationType: data.type, }); return; } - action(payload); + action(data); } /** @@ -65,7 +65,7 @@ function refreshNotificationOptInStatus() { } Log.info('[PushNotification] Push notification opt-in status changed.', false, {isOptedIn}); - PushNotification.setPushNotificationOptInStatus(isOptedIn); + PushNotificationActions.setPushNotificationOptInStatus(isOptedIn); }); } @@ -76,12 +76,12 @@ function refreshNotificationOptInStatus() { * WARNING: Moving or changing this code could break Push Notification processing in non-obvious ways. * DO NOT ALTER UNLESS YOU KNOW WHAT YOU'RE DOING. See this PR for details: https://github.com/Expensify/App/pull/3877 */ -function init() { +const init: Init = () => { // Setup event listeners Airship.addListener(EventType.PushReceived, (notification) => { // By default, refresh notification opt-in status to true if we receive a notification if (!isUserOptedInToPushNotifications) { - PushNotification.setPushNotificationOptInStatus(true); + PushNotificationActions.setPushNotificationOptInStatus(true); } pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload); @@ -97,14 +97,13 @@ function init() { Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus); ForegroundNotifications.configureForegroundNotifications(); -} +}; /** * Register this device for push notifications for the given notificationID. - * - * @param {String|Number} notificationID */ -function register(notificationID) { +const register: Register = (notificationID) => { + // @ts-expect-error FIXME: This condition will never satisfy because Airship.contact.getNamedUserId() returns a promise. if (Airship.contact.getNamedUserId() === notificationID.toString()) { // No need to register again for this notificationID. return; @@ -126,18 +125,18 @@ function register(notificationID) { // Refresh notification opt-in status NVP for the new user. refreshNotificationOptInStatus(); -} +}; /** * Deregister this device from push notifications. */ -function deregister() { +const deregister: Deregister = () => { Log.info('[PushNotification] Unsubscribing from push notifications.'); Airship.contact.reset(); Airship.removeAllListeners(EventType.PushReceived); Airship.removeAllListeners(EventType.NotificationResponse); ForegroundNotifications.disableForegroundNotifications(); -} +}; /** * Bind a callback to a push notification of a given type. @@ -148,45 +147,41 @@ function deregister() { * 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 + * @param triggerEvent - The event that should trigger this callback. Should be one of UrbanAirship.EventType */ -function bind(notificationType, callback, triggerEvent) { - if (!notificationEventActionMap[triggerEvent]) { - notificationEventActionMap[triggerEvent] = {}; +function bind(notificationType: string, callback: NotificationEventActionCallback, triggerEvent: EventType) { + let actionMap = notificationEventActionMap[triggerEvent]; + + if (!actionMap) { + actionMap = {}; } - notificationEventActionMap[triggerEvent][notificationType] = callback; + + actionMap[notificationType] = callback; + notificationEventActionMap[triggerEvent] = actionMap; } /** * 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) { +const onReceived: 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) { +const onSelected: OnSelected = (notificationType, callback) => { bind(notificationType, callback, EventType.NotificationResponse); -} +}; /** * Clear all push notifications */ -function clearNotifications() { +const clearNotifications: ClearNotifications = () => { Airship.push.clearNotifications(); -} +}; -export default { +const PushNotification: PushNotificationType = { init, register, deregister, @@ -195,3 +190,5 @@ export default { TYPE: NotificationType, clearNotifications, }; + +export default PushNotification; diff --git a/src/libs/Notification/PushNotification/index.ts b/src/libs/Notification/PushNotification/index.ts index 88136ff5dc72..1e5499d1fe7d 100644 --- a/src/libs/Notification/PushNotification/index.ts +++ b/src/libs/Notification/PushNotification/index.ts @@ -1,7 +1,8 @@ import NotificationType from './NotificationType'; +import PushNotificationType from './types'; // Push notifications are only supported on mobile, so we'll just noop here -export default { +const PushNotification: PushNotificationType = { init: () => {}, register: () => {}, deregister: () => {}, @@ -10,3 +11,5 @@ export default { TYPE: NotificationType, clearNotifications: () => {}, }; + +export default PushNotification; diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index ce1f013be90f..46f99fcc9271 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,13 +1,8 @@ import {PushPayload} from '@ua/react-native-airship'; -import {OnyxUpdate} from 'react-native-onyx'; import Log from '@libs/Log'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; - -type PushData = { - onyxData: OnyxUpdate[]; - reportID?: number; -}; +import {NotificationData} from './NotificationType'; /** * Returns whether the given Airship notification should be shown depending on the current state of the app @@ -22,15 +17,15 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo payload = JSON.parse(payload); } - const pushData = payload as PushData; + const data = payload as NotificationData; - if (!pushData.reportID) { + if (!data.reportID) { Log.info('[PushNotification] Not a report action notification. Showing notification'); return true; } - const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(pushData.onyxData); - const shouldShow = Report.shouldShowReportActionNotification(String(pushData.reportID), reportAction, true); + const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null); + const shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), reportAction, true); Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`); return shouldShow; } diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index ede873f79c6e..547ecb1de5b2 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -12,7 +12,7 @@ import PushNotification from './index'; export default function subscribeToReportCommentPushNotifications() { 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); + Onyx.update(onyxData ?? []); backgroundRefresh(); }); @@ -33,9 +33,14 @@ export default function subscribeToReportCommentPushNotifications() { } Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); } catch (error) { - Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: error.message}); + let errorMessage = String(error); + if (error instanceof Error) { + errorMessage = error.message; + } + + Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage}); } }); }); diff --git a/src/libs/Notification/PushNotification/types.ts b/src/libs/Notification/PushNotification/types.ts new file mode 100644 index 000000000000..f72ee1af887a --- /dev/null +++ b/src/libs/Notification/PushNotification/types.ts @@ -0,0 +1,22 @@ +import {ValueOf} from 'type-fest'; +import NotificationType, {NotificationDataMap} from './NotificationType'; + +type Init = () => void; +type Register = (notificationID: string | number) => void; +type Deregister = () => void; +type OnReceived = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void; +type OnSelected = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void; +type ClearNotifications = () => void; + +type PushNotification = { + init: Init; + register: Register; + deregister: Deregister; + onReceived: OnReceived; + onSelected: OnSelected; + TYPE: typeof NotificationType; + clearNotifications: ClearNotifications; +}; + +export default PushNotification; +export type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register}; diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index 50b1503b90bd..843d3ae86e46 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -2,9 +2,11 @@ import {OnyxUpdate} from 'react-native-onyx'; import Request from './Request'; import Response from './Response'; +type OnyxServerUpdate = OnyxUpdate & {shouldNotify?: boolean}; + type OnyxUpdateEvent = { eventType: string; - data: OnyxUpdate[]; + data: OnyxServerUpdate[]; }; type OnyxUpdatesFromServer = { @@ -16,4 +18,4 @@ type OnyxUpdatesFromServer = { updates?: OnyxUpdateEvent[]; }; -export type {OnyxUpdatesFromServer, OnyxUpdateEvent}; +export type {OnyxUpdatesFromServer, OnyxUpdateEvent, OnyxServerUpdate}; From 8d989192aa18c66ad0122851d26fdad412155681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 30 Nov 2023 05:40:14 -0300 Subject: [PATCH 07/11] Minor improvements --- src/libs/Notification/PushNotification/index.native.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 537989116021..853edfb1b10c 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -9,13 +9,15 @@ import PushNotificationType, {ClearNotifications, Deregister, Init, OnReceived, type NotificationEventActionCallback = (data: NotificationData) => void; +type NotificationEventActionMap = Partial>>; + let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => (isUserOptedInToPushNotifications = val ?? false), + callback: (value) => (isUserOptedInToPushNotifications = value ?? false), }); -const notificationEventActionMap: Partial>> = {}; +const notificationEventActionMap: NotificationEventActionMap = {}; /** * Handle a push notification event, and trigger and bound actions. From 8d9e2a9d3a691090086698394111dbd4952e7955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 4 Dec 2023 08:25:08 +0000 Subject: [PATCH 08/11] Fix PushNotification register() promise bug --- .../PushNotification/index.native.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 853edfb1b10c..7b2571eea368 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -105,28 +105,34 @@ const init: Init = () => { * Register this device for push notifications for the given notificationID. */ const register: Register = (notificationID) => { - // @ts-expect-error FIXME: This condition will never satisfy because Airship.contact.getNamedUserId() returns a promise. - if (Airship.contact.getNamedUserId() === notificationID.toString()) { - // No need to register again for this notificationID. - return; - } - - // Get permissions to display push notifications (prompts user on iOS, but not Android) - Airship.push.enableUserNotifications().then((isEnabled) => { - if (isEnabled) { - return; - } - - Log.info('[PushNotification] User has disabled visible push notifications for this app.'); - }); - - // Register this device as a named user in AirshipAPI. - // Regardless of the user's opt-in status, we still want to receive silent push notifications. - Log.info(`[PushNotification] Subscribing to notifications`); - Airship.contact.identify(notificationID.toString()); - - // Refresh notification opt-in status NVP for the new user. - refreshNotificationOptInStatus(); + Airship.contact + .getNamedUserId() + .then((userID) => { + if (userID === notificationID.toString()) { + // No need to register again for this notificationID. + return; + } + + // Get permissions to display push notifications (prompts user on iOS, but not Android) + Airship.push.enableUserNotifications().then((isEnabled) => { + if (isEnabled) { + return; + } + + Log.info('[PushNotification] User has disabled visible push notifications for this app.'); + }); + + // Register this device as a named user in AirshipAPI. + // Regardless of the user's opt-in status, we still want to receive silent push notifications. + Log.info(`[PushNotification] Subscribing to notifications`); + Airship.contact.identify(notificationID.toString()); + + // Refresh notification opt-in status NVP for the new user. + refreshNotificationOptInStatus(); + }) + .catch((error) => { + Log.warn('[PushNotification] Failed to register for push notifications! Reason: ', error); + }); }; /** From 72ee22482765151295b6da0f14e675305f37df68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 4 Dec 2023 08:25:35 +0000 Subject: [PATCH 09/11] Mark some report comment notification data params as required --- .../Notification/PushNotification/NotificationType.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts index a69525c96581..91eec6895394 100644 --- a/src/libs/Notification/PushNotification/NotificationType.ts +++ b/src/libs/Notification/PushNotification/NotificationType.ts @@ -11,10 +11,10 @@ type NotificationDataMap = { type NotificationData = ReportCommentNotificationData; type ReportCommentNotificationData = { - title?: string; - type?: typeof NotificationType.REPORT_COMMENT; - reportID?: number; - reportActionID?: string; + title: string; + type: typeof NotificationType.REPORT_COMMENT; + reportID: number; + reportActionID: string; shouldScrollToLastUnread?: boolean; roomName?: string; onyxData?: OnyxServerUpdate[]; From 3db74c00a8f3c3bd1a0fff59de0d182a7d3001b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 4 Dec 2023 08:27:29 +0000 Subject: [PATCH 10/11] Rename ForegroundNotificationsType to ForegroundNotificationsModule --- .../PushNotification/ForegroundNotifications/index.android.ts | 4 ++-- .../PushNotification/ForegroundNotifications/index.ios.ts | 4 ++-- .../PushNotification/ForegroundNotifications/index.ts | 4 ++-- .../PushNotification/ForegroundNotifications/types.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts index 8c63d81093a6..5eef0b44a7d1 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts @@ -1,6 +1,6 @@ import Airship from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; -import ForegroundNotificationsType from './types'; +import ForegroundNotificationsModule from './types'; function configureForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload))); @@ -10,7 +10,7 @@ function disableForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate(() => Promise.resolve(false)); } -const ForegroundNotifications: ForegroundNotificationsType = { +const ForegroundNotifications: ForegroundNotificationsModule = { configureForegroundNotifications, disableForegroundNotifications, }; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts index 588a24a27651..e5e5665d1ea2 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts @@ -1,6 +1,6 @@ import Airship, {iOS} from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; -import ForegroundNotificationsType from './types'; +import ForegroundNotificationsModule from './types'; function configureForegroundNotifications() { // Set our default iOS foreground presentation to be loud with a banner @@ -21,7 +21,7 @@ function disableForegroundNotifications() { Airship.push.iOS.setForegroundPresentationOptionsCallback(() => Promise.resolve([])); } -const ForegroundNotifications: ForegroundNotificationsType = { +const ForegroundNotifications: ForegroundNotificationsModule = { configureForegroundNotifications, disableForegroundNotifications, }; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts index 91a68fbc3503..25baa34099b6 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts @@ -1,9 +1,9 @@ -import ForegroundNotificationsType from './types'; +import ForegroundNotificationsModule from './types'; /** * Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms. */ -const ForegroundNotifications: ForegroundNotificationsType = { +const ForegroundNotifications: ForegroundNotificationsModule = { configureForegroundNotifications: () => {}, disableForegroundNotifications: () => {}, }; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts index ebceed1e4543..f84934651259 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts @@ -1,6 +1,6 @@ -type ForegroundNotifications = { +type ForegroundNotificationsModule = { configureForegroundNotifications: () => void; disableForegroundNotifications: () => void; }; -export default ForegroundNotifications; +export default ForegroundNotificationsModule; From 92559e1111f04f2cb734ffa88055ef0ef435b032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 4 Dec 2023 08:39:26 +0000 Subject: [PATCH 11/11] Fix tests --- __mocks__/@ua/react-native-airship.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__mocks__/@ua/react-native-airship.js b/__mocks__/@ua/react-native-airship.js index 1672c064f9be..29be662e96a1 100644 --- a/__mocks__/@ua/react-native-airship.js +++ b/__mocks__/@ua/react-native-airship.js @@ -31,7 +31,7 @@ const Airship = { }, contact: { identify: jest.fn(), - getNamedUserId: jest.fn(), + getNamedUserId: () => Promise.resolve(undefined), reset: jest.fn(), }, };