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

bug(messaging): notificationActionPerformed not fired on iOS if the system has garbage collected/terminated the WebView on an app in the background #345

Open
jswilliams opened this issue Mar 23, 2023 · 12 comments
Labels

Comments

@jswilliams
Copy link

jswilliams commented Mar 23, 2023

Plugin(s):
Messaging; "@capacitor-firebase/messaging": "^1.4.0", and below

Platform(s):
iOS only

Current behavior:
If a user opens a notification from the Notification Center while the app is in the background, and iOS has terminated the WebView, the notificationActionPerformed listener will not trigger inside the refreshed WebView once capacitor reloads it for you.

Context:

  • iOS will terminate the WebView if the app is in the background to free up system memory, it can do this suddenly and without warning. Apple doesn't publicize the criteria for when the system decides to do this but it happens quite a bit if the user has been using other apps on their phone since the last time your app has been brought to the foreground.
  • If iOS hasn't also terminated the capacitor native app shell, then when the app is brought back to the foreground capacitor calls .reload() on the WebView from ios/Capacitor/Capacitor/WebViewDelegationHandler.swift:webViewWebContentProcessDidTerminate to reload it at the last URL it was on before it was terminated by the system. In this case the notificationActionPerformed listener isn't notified within the WebView since the WebView wasn't available yet when the app came back to the foreground.

Expected behavior:
The notificationActionPerformed listener should be triggered inside the WebView once .reload() completes.

Steps to reproduce:
On iOS, open any Capacitor app and let it finish starting. Move the app to the background. Send a notification and leave it unopened in the Notification Center for now. At this point open quite a few other apps (our goal is to get iOS to automatically garbage collect/terminate the Capacitor app’s WebView for system memory reasons, but not to terminate the native app shell. iOS manages the memory/garbage collection of WebView's separately from your app).

Now if you open the notification from the Notification Center, the app will come to the foreground from the background. But since the WebView was terminated, .reload() gets called on it to refresh the current URL and the notificationActionPerformed listener never fires within the WebView.

Related code:
Reproducible with any implementation of the plugin that uses the notificationActionPerformed listener.

Other information:
This mainly affects users in the following way:

  • The user opened the app earlier in the day to check something
  • The user receives a notification later in the day after they've been using other apps on their phone, your app is in the background, but quite a ways back in the "app switcher stack"
  • When the user opens your notification, nothing happens since the app's custom code inside notificationActionPerformed never has a chance to process the notification

As far as I can tell this seems to affect all of the majority, if not all, notification capacitor plugins out there. This doesn't happen on Android because Android won't ever terminate the WebView independently from the app's native shell.

I'm thinking there should be a way to save the latest opened notification on the native side and rebroadcast it to notificationActionPerformed once the WebView is available again.

Capacitor doctor:

💊   Capacitor Doctor  💊 

Latest Dependencies:

  @capacitor/cli: 4.7.1
  @capacitor/core: 4.7.1
  @capacitor/android: 4.7.1
  @capacitor/ios: 4.7.1

Installed Dependencies:

  @capacitor/cli: 4.3.0
  @capacitor/core: 4.3.0
  @capacitor/android: 4.3.0
  @capacitor/ios: 4.3.0

[success] iOS looking great! 👌
[success] Android looking great! 👌
@jswilliams jswilliams changed the title bug: notificationActionPerformed not fired on iOS if the system has garbage collected/terminated the WebView on an app in the background bug(messaging): notificationActionPerformed not fired on iOS if the system has garbage collected/terminated the WebView on an app in the background Mar 23, 2023
@jswilliams
Copy link
Author

jswilliams commented Mar 24, 2023

To be clear this isn’t necessarily an issue with the plug-in, and more so how Capacitor works on iOS once Apple implemented their more aggressive memory cleanup of backgrounded WebViews. It could be fixed inside the plug-in itself but it’d be kind of hacky and ideally Capacitor would handle this situation for plugins on its own.

Since this plug-in uses retainUntilConsumed: true when calling notifyListeners, users of the plug-in can implement a work around in their own web code. The idea is that Capacitor will retain an event to be emitted if there are 0 active listeners to that event. We can use this to our advantage to get Capacitor to rebroadcast the event only once the new .reload’ed WebView is available.

Here’s how:

  • Set up your notification listeners normally.
  • Now use @capacitor/app’s addListener('appStateChange', ...), to unsubscribe from notificationActionPerfomed when the app goes to the background. And then resubscribe to notificationActionPerformed when the app comes to the foreground.
  • Finally, for good measure, modify your standard notification set up code to call FirebaseMessaging.removeAllListeners before you set up any of your listeners. This is to ensure we clean up any listeners that were around from a potential old WebView (before it reloaded) in the case of a reload happening while the app was in the foreground… Sometimes iOS may destroy (and then Capacitor reloads) the WebView even when it’s in the foreground, although this is a much rarer occurrence than it being destroyed while in the background and is usually due to a memory leak in your web code. This step handles cleaning up the old/inactive listeners as soon as possible in that case.

Once you have this all set up notificationActionPerformed will always fire when expected on iOS, even if the system killed the WebView in the interim.

Hope this helps anybody who has noticed reports of notifications intermittently not opening properly from their iOS users.

@robingenz
Copy link
Member

Thank you for your detailed request. This problem might be related to #210 and #244. I will take a closer look again at all three issues soon.

@robingenz robingenz added the platform: ios iOS platform label Mar 24, 2023
@AnastasiosF
Copy link

Any news about the issue?

@AnastasiosF
Copy link

@jswilliams did you implement that and fixed the issue? I am planing to migrate from official plugin to solve the issue. (Also happened to official push notification plugin). I implements your proposal to official plugin and still the issue not solved.

@jswilliams
Copy link
Author

@AnastasiosF I did, and it fixed the issue in our app. Here's how the workaround is implemented. setupNotifications() gets called one time from app.component.ts's ngOnInit(). Let me know if this helps.

image

@AnastasiosF
Copy link

AnastasiosF commented May 3, 2023

@jswilliams which version of plugin do you use?
I am using capacitor 3 version and it doesn't work. Also notice that "retainUntilConsumed" is missing in that version.
I am talking about "@capacitor-firebase/messaging": "^0.5.1".

Also the same pattern with official plugin doesn't work.

@jswilliams
Copy link
Author

jswilliams commented May 3, 2023

@AnastasiosF We’re using the Capacitor 4 version, @capacitor-firebase/messaging: “^1.4.0”.

The work around definitely relies on retainUntilConsumed being used by the plugin. So you might end up needing to fork 0.5.1 to add retainUntilConsumed to the notificationActionPerformed event, if you can’t upgrade.

I’m still surprised something similar doesn’t also work with the official plug-in if it’s using retainUntilConsumed. Maybe Capacitor 3 handles retainUntilConsumed differently than Capacitor 4?

@AnastasiosF
Copy link

I migrate to Capacitor 4 and implement your solution. I am on testing to see if it works.
However have you find, how to simulate the termination of Webview from system?

@jswilliams
Copy link
Author

The only way I found was trial and error. I had to open the app, put it in the background, and then open anywhere between 18-30 different apps. Then bring my app back to the foreground from the app switcher.

Had to do it a few times before I got lucky and the system had finally terminated the WebView (could tell because there’d be a white flash showing the WebView reloading/the Angular page reloading). If the Splash Screen shows instead then you went too far and the system also terminated the native shell.

You might also try opening the other apps and locking your phone for 10 minutes or so before coming back and checking to give the phone more time to do its clean up.

@AnastasiosF
Copy link

AnastasiosF commented May 4, 2023 via email

@AnastasiosF
Copy link

@jswilliams the issue seems not to be fixed. It's appeared again.

@jswilliams
Copy link
Author

jswilliams commented May 9, 2023

@AnastasiosF do you also have the removeAllListeners() in your set up code, so that it handles if the WebView reloads while it’s in the foreground as well?

None of our users have reported this issue since we changed our implementation to what is in that screenshot. And I’m not able to get it to happen on my test device.

@robingenz robingenz added the bug/fix Something isn't working label Jun 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants