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

Setup push notifications #424

Merged
merged 84 commits into from
Oct 21, 2020
Merged

Setup push notifications #424

merged 84 commits into from
Oct 21, 2020

Conversation

roryabraham
Copy link
Contributor

@roryabraham roryabraham commented Sep 8, 2020

Fixes: #288

Details

  • Adds Airship as a library (urbanairship-react-native)
    • Airship's react native library is only compatible with iOS 11+, so I had to bump the iOS platform target
  • Configures iOS and Android with Airship API keys, capabilities, permissions, etc...
  • Refactors the Notification library to separate out LocalNotifications vs PushNotifications
    • LocalNotifications
      • Are triggered by the client.
      • Are currently only implemented for web/desktop using browser notifications. In the future, LocalNotifications could be implemented for mobile via a growl, snackbar, or using the native notification system (which I think could be done pretty easily using wix/react-native-notifications
    • PushNotifications
      • Are triggered remotely by the Airship API.
      • By default, are only displayed if the app is in the background.
      • Are only implemented for native platforms, and don't really have an equivalent on web/desktop.
      • Furthermore, if the app is in the foreground, any callbacks associated with push notifications will not be executed. We're assuming that if we're receiving a push notification, then we must be connected to the internet, and if we're connected to the internet, then we must be connected to Pusher. And if we're connected to pusher, then the events having to do with Push notifications will be caught and handled via pusher.
  • Gets permission to display push notifications when the app first loads
    • If user has already granted or denied permissions for user notifications, this code not affect the user at all.

    • From the Airship docs:

      The Airship module makes a distinction between 'user notifications,' which can be seen by the user, and 'invisible notifications' that carry only data for the app to process.

    • Even if the user denies permission for 'user notifications', we will still be able to send push notifications to trigger update actions in the background.

  • On sign-in, registers the device for push notifications (associates the device and accountID with a named_user in the Airship API) On sign-out, deregisters the device for push notifications (associates the device with an undefined named_user in the Airship API)
    • To implement this, we simply subscribe the push notification library to IONKEYS.SESSION.accountID. When the user signs in, the SESSION.accountID is assigned, and the registration is triggered. When the user signs out, we clear Ion, the SESSION.accountID is nullified, and the deregistration is triggered. However, we weren't executing the callbacks for Ion subscribers when we called Ion.clear(), so this PR implements that as well.

Tests / QA

  1. Build the mobile app to a physical iPhone device (instructions here). (dev) Or just open the iPhone app 😉 (QA)
  2. Log into an account and background the app.
  3. Go to expensify.com.dev as a different user, and submit an expense report to the account you used in step 2.
  4. Comment on the report, and verify that you receive a push notification on your device with the comment information.
  5. Open the app, and verify that you see the report comment from step 4.
  6. Sign into an account.
  7. Run npm run desktop, and use the desktop UI to sign into another account.
  8. Send a chat from the desktop account to the account you used on mobile, with the app open on mobile. Ensure that you do not see a push notification for the chat.
  9. Send a chat from the desktop account to the account you used on mobile, with the app backgrounded on mobile. Ensure that you see a push notification.
  10. Close the app on mobile. (i.e: actually close it by swiping up and then swiping it away, don't just background it)
  11. Send a chat from the desktop account to the account you used on mobile. Ensure that you see a push notification.
  12. Click on the push notification, and see that it opens up the app to the correct chat.
  13. (dev) Find the accountID of the account you signed in on in mobile, and find a report ID for any of its reports. Then run $EXPENSIDEV/script/clitools push:reportComment, follow the prompts, and verify that you see the push notification show up.
  14. Log out on mobile.
  15. Send a chat from the web account to the account you used on mobile. Ensure that you don't see a push notification.
  16. Build the mobile app to an Android emulator, and repeat steps 2-15.

@roryabraham roryabraham self-assigned this Sep 8, 2020
@botify botify requested review from thienlnam and removed request for a team September 8, 2020 21:27
# Conflicts:
#	ios/Podfile.lock
#	ios/ReactNativeChat.xcodeproj/project.pbxproj
#	src/Expensify.js
#	src/lib/Notification/BrowserNotifications.js
@roryabraham roryabraham changed the title [WIP] Setup push notifications Setup push notifications Sep 15, 2020
@roryabraham roryabraham changed the title Setup push notifications [WIP] Setup push notifications Sep 15, 2020
@AndrewGable
Copy link
Contributor

Regarding your discussion: I'd suggest we just follow what the API currently does which triggers native (iOS/Android) notifications from our Expensify API to the Urban Airship API. Then keep the web browser the same using local notifications via pusher.

The only difference I'd suggest is let's create a new project in Airship and send these pushes separately from our main Expensify app/Airship keys.

@roryabraham
Copy link
Contributor Author

roryabraham commented Sep 22, 2020

So today has been pretty successful in that I've been able to get test push notifications showing up on the device. I've also set up notifications to appear in the foreground by default, but we will likely not want to show push notifications for chat reports which are already visible.

I've also laid the groundwork by which the currently signed-in user registers for Airship notifications. We probably don't need to worry about edge cases where a copilot logs in, right?

I'm unsure how to reconcile differences between pusher and Airship, because they serve similar purposes in our united codebase. For example, if a chat is sent, our API would send a notification via pusher and Airship, and the app needs to be able to determine which notification to respond to/display. Here are some ideas:

  1. Only use pusher for web/desktop, and Airship for mobile. We could lump both of these into a single "notification" library, with the other areas of code being unaware of which is used under the hood.
  2. Ignore/don't show any Airship notifications if the app is connected to Pusher. If connected to pusher, trigger native notifications locally (I think I have figured out how to do this using another library)

@tgolen
Copy link
Contributor

tgolen commented Sep 22, 2020

I think 1 sounds good, but the biggest problem with that is it will only work when notifications are enabled on mobile (and those can be easily disabled), so I think we need to favor relying on Pusher as much as we can (this would favor 2).

@marcaaron
Copy link
Contributor

I think we might also experience a lot of latency since all data would be arriving via Airship on mobile which I'd guess is "less real-time" than Pusher (but open to being proved wrong on that).

@AndrewGable
Copy link
Contributor

AndrewGable commented Sep 22, 2020

We probably don't need to worry about edge cases where a copilot logs in, right?

Agreed, I don't think co-pilot is on the road map.

Ignore/don't show any Airship notifications if the app is connected to Pusher. If connected to pusher, trigger native notifications locally

This feels like the better option to me (and it sounds like others agree on this being better as well)

@AndrewGable
Copy link
Contributor

AndrewGable commented Sep 22, 2020

How do we handle 2 on the current Expensify app? We have both Airship and Pusher co-exist.

@marcaaron
Copy link
Contributor

Ignore/don't show any Airship notifications if the app is connected to Pusher. If connected to pusher, trigger native notifications locally (I think I have figured out how to do this using another library)

Actually, I don't really even think this is an option. Local notifications are only relevant if the app is not in the foreground. If it is then we don't want to show them. At least last time we looked into this Pusher doesn't seem to work at all once an app is backgrounded.

@marcaaron
Copy link
Contributor

IIRC here's what we do on mobile today:

  1. Connect to Pusher when the app is in the foreground (and maybe only when a report is in view?)
  2. When the app is not in the foreground device notifications are shown
  3. When the app is in the foreground and receives an urban airship notification we conditionally display it as a snack bar notification, but never show it as a local notification. If we are looking at the report the update belongs to we do nothing with the airship notification.

@roryabraham
Copy link
Contributor Author

Actually, I don't really even think this is an option. Local notifications are only relevant if the app is not in the foreground. If it is then we don't want to show them.

I was able to get remote push notifications displaying in the foreground earlier today (although I read it's only possible on iOS 10+). I think we could get this working with local notifications too.

@@ -14,6 +14,8 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:3.5.3")
classpath("com.google.gms:google-services:4.2.0")
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to use 4.3.4 here? Just asking b/c I'm also adding this in this PR

Copy link
Contributor Author

@roryabraham roryabraham Oct 14, 2020

Choose a reason for hiding this comment

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

I have no opinion here - again, I didn't write any of this config stuff manually.

# Conflicts:
#	ios/ReactNativeChat.xcodeproj/project.pbxproj
#	src/lib/API.js
#	src/lib/actions/Report.js
@roryabraham
Copy link
Contributor Author

roryabraham commented Oct 14, 2020

Went through and fixed a few more bugs, retested on all platforms, and then merged master. Ready for more reviews 👍

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

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

Code LGTM, found an issue during tests:

  1. Push notifications are showing up on my physical iPhone even when I have the chat open. If I am on Account A on mobile, viewing a chat with Account B, and I type a message on web from Account B, the message shows up on mobile and then a few seconds later a push notification appears for that message.
  2. If I am on Account A viewing a chat with Account B, and Account C sends me a message, I see a push notification. Tapping on that push notification doesn't bring me to the chat w/ Account C. I'm assuming there just shouldn't be a notification shown in this case, and if so the fix for 1 will remove this case

@roryabraham
Copy link
Contributor Author

Hmmm that's pretty strange: at one point I had to intentionally write code to make notifications appear in the foreground, and since removing it I haven't seen any appear in the foreground. I'll have to look into that. #2 is feature, not a bug haha (if we receive a push notification while the app is in the foreground, we assume we're connected to pusher so we don't execute any callbacks)

@roryabraham
Copy link
Contributor Author

Push notifications are showing up on my physical iPhone even when I have the chat open.

Okay, so I'm seeing the same thing on my end, but I'm not sure how to resolve it. This is controlled by the OS, and I think it is a problem with UrbanAirship's React Native SDK. So I opened a GH issue to hopefully get a solution. You can read up on the issue for steps I took to attempt to resolve this, and if you have any other ideas for things I could try I'm all ears.

In the meantime, maybe what we should do is allow callbacks to execute for notifications received in the foreground so that tapping on the notification will open the correct report, then merge this PR and create a separate issue to disable foreground notifications. In my opinion, the benefits of having functional push notifications probably outweigh the potential inconveniences of having them display in the foreground while I investigate and try to resolve it.

@marcaaron
Copy link
Contributor

the benefits of having functional push notifications probably outweigh the potential inconveniences of having them display in the foreground while I investigate and try to resolve it

Does this mean that when you are chatting with someone each new message from them will trigger a notification in the foreground?

@roryabraham
Copy link
Contributor Author

Does this mean that when you are chatting with someone each new message from them will trigger a notification in the foreground?

Yes, on iOS

if (AppState.currentState === 'active' && eventType === EventType.PushReceived) {
console.debug('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.');
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm understanding this problem correctly... I think we could handle this before calling pushNotificationEventCallback(EventType.PushReceived, notification).

There's a lot of explanation going on here and I think at the end of the day all that needs to be said is what was there already. The selected event should always do the callback. Said another way, whether or not notifications show in the foreground wouldn't make a difference - they just happen to be showing and they should not show.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Scaled back this comment because you're right, we should only repress the callback for PushReceived events because those are the only ones that will be duplicates with pusher

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify I was suggesting we do something like this...

UrbanAirship.addListener(EventType.PushReceived, (notification) => {
    // If a push notification is received while the app is in foreground,
    // we'll assume pusher is connected so we'll ignore is and not fetch the same data twice.
    if (AppState.currentState === 'active') {
        console.debug('[PUSH_NOTIFICATION] Push received while app is in foreground, not executing any callback.');
        return;
    }

    pushNotificationEventCallback(EventType.PushReceived, notification);
});

and remove everything from this method.

@roryabraham
Copy link
Contributor Author

Following this discussion I've set up push notification callbacks so that NotificationResponse event callbacks can be triggered when the app is in the foreground. So if you have chat A open and you get a foreground notification for chat B, then tapping it will open chat B. If you have chat A open and you get a foreground notification for chat A, then tapping it has no effect. We still do not allow callbacks to execute on a PushReceived event when the app is in the foreground, because we are operating under the assumption that pusher is also connected so we don't need to fetch the same data twice.

All this is to say, this is ready for another round of reviews. After a big merge, I retested all platforms and aside from the known issue with foreground notifications everything is working as expected.

@roryabraham roryabraham changed the title Setup push notifications [HOLD] Setup push notifications Oct 20, 2020
@roryabraham
Copy link
Contributor Author

roryabraham commented Oct 20, 2020

Actually putting this on HOLD because it sounds like a fix for this should be pretty imminent, according to this comment

@marcaaron
Copy link
Contributor

FYI don't mind where this landed and would merge as is. Once the lib is patched I'm pretty sure the solution will be to just "update" it.

@roryabraham roryabraham changed the title [HOLD] Setup push notifications Setup push notifications Oct 21, 2020
@roryabraham
Copy link
Contributor Author

Okay, sure. Let's do it 👍

this.animationTranslateX = new Animated.Value(!this.state.hamburgerShown ? -300 : 0);

// Note: This null check is only necessary because withIon passes null for bound props
// that are null-initialized initialized in Ion, and defaultProps only replaces for `undefined` values
Copy link
Contributor

Choose a reason for hiding this comment

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

We should make a follow up issue for this and discuss. This keeps popping up.

@marcaaron marcaaron merged commit f9b6077 into master Oct 21, 2020
@marcaaron marcaaron deleted the Rory-PushNotifications branch October 21, 2020 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants