diff --git a/src/libs/Notification/PushNotification/getPushNotificationData.ts b/src/libs/Notification/PushNotification/getPushNotificationData.ts new file mode 100644 index 000000000000..7738cf39fe00 --- /dev/null +++ b/src/libs/Notification/PushNotification/getPushNotificationData.ts @@ -0,0 +1,15 @@ +import type {PushPayload} from '@ua/react-native-airship'; +import type {PushNotificationData} from './NotificationType'; + +function getPushNotificationData(notification: PushPayload): PushNotificationData { + let payload = notification.extras.payload; + + // On Android, some notification payloads are sent as a JSON string rather than an object + if (typeof payload === 'string') { + payload = JSON.parse(payload); + } + + return payload as PushNotificationData; +} + +export default getPushNotificationData; diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 34699f0610e1..9cf38a6dcca0 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -5,6 +5,7 @@ import Log from '@libs/Log'; import * as PushNotificationActions from '@userActions/PushNotification'; import ONYXKEYS from '@src/ONYXKEYS'; import ForegroundNotifications from './ForegroundNotifications'; +import getPushNotificationData from './getPushNotificationData'; import type {PushNotificationData} from './NotificationType'; import NotificationType from './NotificationType'; import type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types'; @@ -27,14 +28,7 @@ const notificationEventActionMap: NotificationEventActionMap = {}; */ 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 (typeof payload === 'string') { - payload = JSON.parse(payload); - } - - const data = payload as PushNotificationData; + const data = getPushNotificationData(notification); Log.info(`[PushNotification] Callback triggered for ${eventType}`); diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts index ec18f776d2d2..638a08ecf9f7 100644 --- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts +++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts @@ -1,8 +1,12 @@ +import Airship from '@ua/react-native-airship'; import Onyx from 'react-native-onyx'; import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; +import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; +import * as DeferredOnyxUpdates from '@libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import getPushNotificationData from '@libs/Notification/PushNotification/getPushNotificationData'; import type {ReportActionPushNotificationData} from '@libs/Notification/PushNotification/NotificationType'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; @@ -13,6 +17,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; +import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PushNotification from '..'; @@ -27,6 +32,20 @@ Onyx.connect({ }, }); +function buildOnyxUpdatesFromServer({onyxData, lastUpdateID, previousUpdateID}: {onyxData: OnyxServerUpdate[]; lastUpdateID: number; previousUpdateID: number}) { + return { + type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, + lastUpdateID, + previousUpdateID, + updates: [ + { + eventType: 'eventType', + data: onyxData, + }, + ], + } as OnyxUpdatesFromServer; +} + function getLastUpdateIDAppliedToClient(): Promise { return new Promise((resolve) => { Onyx.connect({ @@ -50,17 +69,8 @@ function applyOnyxData({reportID, reportActionID, onyxData, lastUpdateID, previo } 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, - }, - ], - }; + + const updates = buildOnyxUpdatesFromServer({onyxData, lastUpdateID, previousUpdateID}); /** * When this callback runs in the background on Android (via Headless JS), no other Onyx.connect callbacks will run. This means that @@ -71,16 +81,51 @@ function applyOnyxData({reportID, reportActionID, onyxData, lastUpdateID, previo } function navigateToReport({reportID, reportActionID}: ReportActionPushNotificationData): Promise { - Log.info('[PushNotification] Navigating to report', false, {reportID, reportActionID}); + Log.info('[PushNotification] Adding push notification updates to deferred updates queue', false, {reportID, reportActionID}); + + // Onyx data from push notifications might not have been applied when they were received in the background + // due to OS limitations. So we'll also attempt to apply them here so they can display immediately. Reliable + // updates will prevent any old updates from being duplicated and any gaps in them will be handled + Airship.push + .getActiveNotifications() + .then((notifications) => { + + if (notifications.length === 0) { + return; + } + + const onyxUpdates = notifications.reduce((updates, notification) => { + const pushNotificationData = getPushNotificationData(notification); + const lastUpdateID = pushNotificationData.lastUpdateID; + const previousUpdateID = pushNotificationData.previousUpdateID; + + if (pushNotificationData.onyxData == null || lastUpdateID == null || previousUpdateID == null) { + return updates; + } + + const newUpdates = buildOnyxUpdatesFromServer({onyxData: pushNotificationData.onyxData, lastUpdateID, previousUpdateID}); - const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); - const report = getReport(reportID.toString()); - const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; - const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); + // eslint-disable-next-line no-param-reassign + updates[lastUpdateID] = newUpdates; + return updates; + }, {}); - Navigation.isNavigationReady() + if (Object.keys(onyxUpdates).length === 0) { + return; + } + + DeferredOnyxUpdates.enqueueAndProcess(onyxUpdates); + }) + .then(Navigation.isNavigationReady) .then(Navigation.waitForProtectedRoutes) .then(() => { + 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); + // The attachment modal remains open when navigating to the report so we need to close it Modal.close(() => { try { diff --git a/src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts index 8a7b67db30c6..a94880ea057b 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; +import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; @@ -59,11 +60,15 @@ function isEmpty() { * Manually processes and applies the updates from the deferred updates queue. (used e.g. for push notifications) */ function process() { + Log.info('[DeferredOnyxUpdates] Processing manually enqueued updates', false, {lastUpdateIDs: Object.keys(deferredUpdates)}); + if (missingOnyxUpdatesQueryPromise) { missingOnyxUpdatesQueryPromise.finally(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates); } missingOnyxUpdatesQueryPromise = OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(); + + return missingOnyxUpdatesQueryPromise; } type EnqueueDeferredOnyxUpdatesOptions = { @@ -83,9 +88,13 @@ function enqueue(updates: OnyxUpdatesFromServer | DeferredUpdatesDictionary, opt // We check here if the "updates" param is a single update. // If so, we only need to insert one update into the deferred updates queue. if (isValidOnyxUpdateFromServer(updates)) { + Log.info('[DeferredOnyxUpdates] Manually enqueuing update', false, {lastUpdateID: updates.lastUpdateID}); + const lastUpdateID = Number(updates.lastUpdateID); deferredUpdates[lastUpdateID] = updates; } else { + Log.info('[DeferredOnyxUpdates] Manually enqueuing updates', false, {lastUpdateIDs: Object.keys(updates)}); + // If the "updates" param is an object, we need to insert multiple updates into the deferred updates queue. Object.entries(updates).forEach(([lastUpdateIDString, update]) => { const lastUpdateID = Number(lastUpdateIDString); @@ -104,7 +113,7 @@ function enqueue(updates: OnyxUpdatesFromServer | DeferredUpdatesDictionary, opt */ function enqueueAndProcess(updates: OnyxUpdatesFromServer | DeferredUpdatesDictionary, options?: EnqueueDeferredOnyxUpdatesOptions) { enqueue(updates, options); - process(); + return process(); } type ClearDeferredOnyxUpdatesOptions = { diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index ffc8cbd989df..85294a822b23 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -85,7 +85,7 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdate function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousParams?: {newLastUpdateIDFromClient: number; latestMissingUpdateID: number}): Promise { const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; - Log.info('[DeferredUpdates] Processing deferred updates', false, {lastUpdateIDFromClient, previousParams}); + Log.info('[DeferredOnyxUpdates] Processing deferred updates', false, {lastUpdateIDFromClient, previousParams}); // We only want to apply deferred updates that are newer than the last update that was applied to the client. // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. @@ -102,7 +102,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousPa // If we detected a gap in the deferred updates, only apply the deferred updates before the gap, // re-fetch the missing updates and then apply the remaining deferred updates after the gap if (latestMissingUpdateID) { - Log.info('[DeferredUpdates] Gap detected in deferred updates', false, {lastUpdateIDFromClient, latestMissingUpdateID}); + Log.info('[DeferredOnyxUpdates] Gap detected in deferred updates', false, {lastUpdateIDFromClient, latestMissingUpdateID}); return new Promise((resolve, reject) => { DeferredOnyxUpdates.clear({shouldUnpauseSequentialQueue: false, shouldResetGetMissingOnyxUpdatesPromise: false}); @@ -127,7 +127,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousPa // Prevent info loops of calls to GetMissingOnyxMessages if (previousParams?.newLastUpdateIDFromClient === newLastUpdateIDFromClient && previousParams?.latestMissingUpdateID === latestMissingUpdateID) { - Log.info('[DeferredUpdates] Aborting call to GetMissingOnyxMessages, repeated params', false, {lastUpdateIDFromClient, latestMissingUpdateID, previousParams}); + Log.info('[DeferredOnyxUpdates] Aborting call to GetMissingOnyxMessages, repeated params', false, {lastUpdateIDFromClient, latestMissingUpdateID, previousParams}); resolve(undefined); return; }