Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide notifications for the currently open report #15858

Merged
merged 24 commits into from
Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a9ae47
pull out notification checks into helper function
arosiclair Mar 14, 2023
3c3dc24
use shouldShowReportActionNotification for push notifications on android
arosiclair Mar 14, 2023
e0626ea
show notifications from concierge
arosiclair Mar 14, 2023
be57390
use casing consistent with other logging
arosiclair Mar 14, 2023
d283491
don't show notifications for report actions that were already read
arosiclair Mar 15, 2023
085e59d
Merge branch 'main' into arosiclair-airship-15
arosiclair Mar 15, 2023
8916cdb
fix camera-roll import
arosiclair Mar 15, 2023
f03189f
fix iOS notification presentation options for v15
arosiclair Mar 15, 2023
6abdfcd
use context awareness for Android and iOS push notifications
arosiclair Mar 15, 2023
c074d49
update comments
arosiclair Mar 15, 2023
a9647a1
update pods
arosiclair Mar 15, 2023
1616a47
clearer logging for debugging
arosiclair Mar 15, 2023
f3279a2
handle push notifications with no reportAction in payload
arosiclair Mar 16, 2023
20b3c82
use report action from onyxData
arosiclair Mar 16, 2023
48128a8
use platform modules to fix errors about using iOS/Android SDKs on th…
arosiclair Mar 16, 2023
9fa14dd
add airship mocks for foreground callbacks
arosiclair Mar 17, 2023
fc6ebf8
mock new camera roll lib
arosiclair Mar 17, 2023
9dc1ec4
fix require cycle between PushNotification libs and Report lib
arosiclair Mar 17, 2023
e9d7f4c
Merge branch 'main' of github.com:Expensify/App into arosiclair-airsh…
arosiclair Mar 30, 2023
8b08eb3
remove unnecessary docs
arosiclair Mar 30, 2023
130c0b0
add a note about this possibly being fixed in the future
arosiclair Mar 30, 2023
4419098
Merge branch 'main' of github.com:Expensify/App into arosiclair-airsh…
arosiclair Apr 5, 2023
5405f30
clarify setForegroundPresentationOptionsCallback behavior
arosiclair Apr 5, 2023
fd0afba
Merge branch 'main' of github.com:Expensify/App into arosiclair-airsh…
arosiclair Apr 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions __mocks__/@ua/react-native-airship.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const Airship = {
iOS: {
setBadgeNumber: jest.fn(),
setForegroundPresentationOptions: jest.fn(),
setForegroundPresentationOptionsCallback: jest.fn(),
},
android: {
setForegroundDisplayPredicate: jest.fn(),
},
enableUserNotifications: () => Promise.resolve(false),
clearNotifications: jest.fn(),
Expand Down
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ dependencies {
implementation project(':react-native-plaid-link-sdk')

// Fixes a version conflict between airship and react-native-plaid-link-sdk
// This may be fixed by a newer version of the plaid SDK (not working as of 10.0.0)
implementation "androidx.work:work-runtime-ktx:2.8.0"

// This okhttp3 dependency prevents the app from crashing - See https://github.com/plaid/react-native-plaid-link-sdk/issues/74#issuecomment-648435002
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Airship from '@ua/react-native-airship';
import shouldShowPushNotification from '../shouldShowPushNotification';

export default function configureForegroundNotifications() {
Airship.push.android.setForegroundDisplayPredicate(pushPayload => Promise.resolve(shouldShowPushNotification(pushPayload)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Airship, {iOS} from '@ua/react-native-airship';
import shouldShowPushNotification from '../shouldShowPushNotification';

export default function configureForegroundNotifications() {
// Set our default iOS foreground presentation to be loud with a banner
// More info here https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter
Airship.push.iOS.setForegroundPresentationOptions([
iOS.ForegroundPresentationOption.List,
iOS.ForegroundPresentationOption.Banner,
iOS.ForegroundPresentationOption.Sound,
iOS.ForegroundPresentationOption.Badge,
]);

// Set a callback to override our foreground presentation per notification depending on the app's current state.
// Returning null keeps the default presentation. Returning [] uses no presentation (hides the notification).
Airship.push.iOS.setForegroundPresentationOptionsCallback(pushPayload => Promise.resolve(shouldShowPushNotification(pushPayload) ? null : []));
arosiclair marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms.
*/
export default function () {}
32 changes: 12 additions & 20 deletions src/libs/Notification/PushNotification/index.native.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import _ from 'underscore';
import Onyx from 'react-native-onyx';
import Airship, {EventType, iOS} from '@ua/react-native-airship';
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';

let isUserOptedInToPushNotifications = false;
Onyx.connect({
Expand All @@ -30,21 +31,21 @@ function pushNotificationEventCallback(eventType, notification) {
payload = JSON.parse(payload);
}

Log.info(`[PUSH_NOTIFICATION] Callback triggered for ${eventType}`);
Log.info(`[PushNotification] Callback triggered for ${eventType}`);
arosiclair marked this conversation as resolved.
Show resolved Hide resolved

if (!payload) {
Log.warn('[PUSH_NOTIFICATION] Notification has null or undefined payload, not executing any callback.');
Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.');
return;
}

if (!payload.type) {
Log.warn('[PUSH_NOTIFICATION] No type value provided in payload, not executing any callback.');
Log.warn('[PushNotification] No type value provided in payload, not executing any callback.');
return;
}

const action = actionMap[payload.type];
if (!action) {
Log.warn('[PUSH_NOTIFICATION] No callback set up: ', {
Log.warn('[PushNotification] No callback set up: ', {
event: eventType,
notificationType: payload.type,
});
Expand All @@ -64,13 +65,13 @@ function refreshNotificationOptInStatus() {
return;
}

Log.info('[PUSH_NOTIFICATION] Push notification opt-in status changed.', false, {isOptedIn});
Log.info('[PushNotification] Push notification opt-in status changed.', false, {isOptedIn});
PushNotification.setPushNotificationOptInStatus(isOptedIn);
});
}

/**
* Register push notification callbacks. This is separate from namedUser registration because it needs to be executed
* Configure push notifications and register callbacks. This is separate from namedUser registration because it needs to be executed
* from a headless JS process, outside of any react lifecycle.
*
* WARNING: Moving or changing this code could break Push Notification processing in non-obvious ways.
Expand All @@ -96,16 +97,7 @@ function init() {
// Keep track of which users have enabled push notifications via an NVP.
Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus);

// This statement has effect on iOS only.
// It enables the App to display push notifications when the App is in foreground.
// By default, the push notifications are silenced on iOS if the App is in foreground.
// More info here https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter
Airship.push.iOS.setForegroundPresentationOptions([
iOS.ForegroundPresentationOption.List,
iOS.ForegroundPresentationOption.Banner,
iOS.ForegroundPresentationOption.Sound,
iOS.ForegroundPresentationOption.Badge,
]);
configureForegroundNotifications();
}

/**
Expand All @@ -126,12 +118,12 @@ function register(accountID) {
return;
}

Log.info('[PUSH_NOTIFICATIONS] User has disabled visible push notifications for this app.');
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(`[PUSH_NOTIFICATIONS] Subscribing to notifications for account ID ${accountID}`);
Log.info(`[PushNotification] Subscribing to notifications for account ID ${accountID}`);
Airship.contact.identify(accountID.toString());

// Refresh notification opt-in status NVP for the new user.
Expand All @@ -142,7 +134,7 @@ function register(accountID) {
* Deregister this device from push notifications.
*/
function deregister() {
Log.info('[PUSH_NOTIFICATIONS] Unsubscribing from push notifications.');
Log.info('[PushNotification] Unsubscribing from push notifications.');
Airship.contact.reset();
Airship.removeAllListeners(EventType.PushReceived);
Airship.removeAllListeners(EventType.NotificationResponse);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import _ from 'underscore';
import * as Report from '../../actions/Report';
import Log from '../../Log';
import * as ReportActionUtils from '../../ReportActionsUtils';

/**
* 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) {
Log.info('[PushNotification] push notification received', false, {pushPayload});

let pushData = pushPayload.extras.payload;

// The payload is string encoded on Android
if (_.isString(pushData)) {
pushData = JSON.parse(pushData);
}

if (!pushData.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);
Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`);
return shouldShow;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {CONST} from 'expensify-common/lib/CONST';
import {Linking} from 'react-native';
import Onyx from 'react-native-onyx';
import PushNotification from '.';
import ROUTES from '../../../ROUTES';
import Log from '../../Log';
import Navigation from '../../Navigation/Navigation';

/**
* Setup reportComment push notification callbacks.
*/
export default function subscribeToReportCommentPushNotifications() {
PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, onyxData}) => {
Log.info('[Report] Handled event sent by Airship', false, {reportID});
Onyx.update(onyxData);
});

// Open correct report when push notification is clicked
PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, ({reportID}) => {
if (Navigation.canNavigate('navigate')) {
// If a chat is visible other than the one we are trying to navigate to, then we need to navigate back
if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) {
Navigation.goBack();
}
Navigation.isDrawerReady()
.then(() => {
Navigation.navigate(ROUTES.getReportRoute(reportID));
});
} else {
// Navigation container is not yet ready, use deeplinking to open to correct report instead
Navigation.setDidTapNotification();
Navigation.isDrawerReady()
.then(() => {
Linking.openURL(`${CONST.DEEPLINK_BASE_URL}${ROUTES.getReportRoute(reportID)}`);
});
}
});
}
17 changes: 17 additions & 0 deletions src/libs/ReportActionsUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,22 @@ function getLastClosedReportAction(reportActions) {
return lodashFindLast(sortedReportActions, action => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED);
}

/**
* @param {Array} onyxData
* @returns {Object} The latest report action in the `onyxData` or `null` if one couldn't be found
*/
function getLatestReportActionFromOnyxData(onyxData) {
const reportActionUpdate = _.find(onyxData, onyxUpdate => onyxUpdate.key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS));

if (!reportActionUpdate) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB: In general I think we prefer keeping the return type consistent by using an empty object instead, but it's not a big deal.

}

const reportActions = _.values(reportActionUpdate.value);
const sortedReportActions = getSortedReportActions(reportActions);
return _.last(sortedReportActions);
}

export {
getSortedReportActions,
getLastVisibleAction,
Expand All @@ -237,4 +253,5 @@ export {
isConsecutiveActionMadeByPreviousActor,
getSortedReportActionsForDisplay,
getLastClosedReportAction,
getLatestReportActionFromOnyxData,
};
Loading