ionic-team / capacitor

Build cross-platform Native Progressive Web Apps for iOS, Android, and the Web ⚡️
https://capacitorjs.com
MIT License
11.46k stars 977 forks source link

[DOC NEEDED] - How to implement a custom notifications delegate on iOS? #1631

Closed rpanadero closed 4 years ago

rpanadero commented 5 years ago

Description of the problem:

First of all, I'm sorry if this shouldn't be reported as bug report, but I need help with this matter as soon as possible.

The problem that I'm facing is related to having a custom notifications delegate on iOS. I have been having a look at Capacitor core and I have seen that there is a delegate set for UNUserNotificationCenter. However, this delegate implementation doesn't fit me because my app business requirements need another one. How could I override this implementation? If I try to register another delegate in my application AppDelegate, which is versioned, it doesn't work because Capacitor core registers its delegate later.

On the other hand, I does be able to do what I want on Android by registering a custom FirebaseMessagingService in AndroidManifest.xml. In addition, I can even call Capacitor Push Notifications plugin to avoid breaking its implementation.

public class MyAppFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onNewToken(String newToken) {
        super.onNewToken(newToken);
        PushNotifications.onNewToken(newToken);
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        PushNotifications.sendRemoteMessage(remoteMessage);
        // I can do whatever I want
    }

}
<service android:name="com.demo.myapp.services.MyAppFirebaseMessagingService" android:stopWithTask="false">
    <intent-filter android:priority="9999">
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Affected platform

OS of the development machine

Other information:

Capacitor version: 1.0

node version: 11.4.0

npm version: 6.5.0

CocoaPods version: 1.7.1

rpanadero commented 5 years ago

By the way, I have found out another issue related to the iOS notifications. The application is not notified by any Capacitor push notification callback when a notification is received while the application is in background. In addition, if the notification is "content-available" the application is not notified either in background or foreground.

In order to support this iOS feature it should be necessary to implement this method of AppDelegate:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);
gubbigubbi commented 5 years ago

Hey @rpanadero,

I have found this issue too. My iOS app is not receiving FCM push notifications. It does when I send the test message to the entire app, but not individual messages, which are working on Android.

How do we implement your code you mentioned above?

philet commented 5 years ago

However, this delegate implementation doesn't fit me because my app business requirements need another one. How could I override this implementation? If I try to register another delegate in my application AppDelegate, which is versioned, it doesn't work because Capacitor core registers its delegate later.

This is an issue for me too. Any plan on supporting custom UNUserNotificationCenterDelegate implementations anytime soon?

Thanks!

jcesarmobile commented 5 years ago

how are you doing the custom UNUserNotificationCenterDelegate? with a plugin? custom code? could you provide a sample? What do you need to do that the Capacitor one can't?

If you do it in the load method of a custom plugin it should be called after the CAPUNUserNotificationCenterDelegate one and use yours.

philet commented 5 years ago

I'm working on a plugin for the Salesforce MarketingCloudSDK (https://salesforce-marketingcloud.github.io/MarketingCloudSDK-iOS/get-started/apple.html).

As per the instructions of this SDK (and also the Apple iOS documentation*), the UNUserNotificationCenterDelegate must be set in "application(_:didFinishLaunchingWithOptions:)". The problem is, that CAPUNUserNotificationCenterDelegate sets itself as the delegate after this. So there is no way for me to set my custom delegate at the right point without it being "overwritten" by Capacitor afterwards.

* See the "Important" box here: https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate

rpanadero commented 5 years ago

Hi @jcesarmobile,

I tried it with custom code and it happens what @philet says. Capacitor registers his delegate after mine it is set, so we could say that yours have more priority.

My problem was the same as @philet describes above. My notifications SDK required that my application called a specific method on willPresentNotification of UNUserNotificationCenterDelegate, just to notify the SDK of the push reception.

Finally, as I could do that easily, I decided to just use the Capacitor Core for push notifications handling and notify the notifications SDK from hybrid side by calling a plugin method. Maybe, it is not the best approach, but I guess that it is the easiest way to avoid having the same problem we had on Cordova when we had multiple notifications plugins which overrided the same delegate methods by doing swizzling. That experience on Cordova was awful because I was always frightened of breaking something.

If you let me suggest something, I think that another possible approach could be to implement the UNUserNotificationCenterDelegate in AppDelegate and emit an event when the method is fired. Something similar to what you are doing with didRegisterForRemoteNotificationsWithDeviceToken.

https://github.com/ionic-team/capacitor/blob/master/ios-template/App/App/AppDelegate.swift#L64

Like that, you could catch this event in CAPUNUserNotificationCenterDelegate, propagate the events to the respective methods and notify the web side by the Capacitor bridge. In addition, this would allow developers to put its custom code into AppDelegate methods accepting responsibility.

On the other hand, I remember you that I also realized that you are not implementing in AppDelegate:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);

So, if I receive a silent push notification my application is not notified and I cannot handle it. And the same happens, if the push notification is received in background although it is a non-silent one.

Feel free to tell me anything about my suggestions.

Thanks in advance

jcesarmobile commented 4 years ago

I've sent a PR to not override the UNUserNotificationCenterDelegate if present, so people can use custom delegates if they want.

I don't like the idea of moving the whole UNUserNotificationCenterDelegate to the AppDelegate, we would like to keep the AppDelegate as clean as possible, the other methods that we've put there was because they are AppDelegate methods, but UNUserNotificationCenterDelegate can be any class. But we have to find a way of making easier to use a custom UNUserNotificationCenterDelegate but at the same time make it easy to propagate the data to the Push/Notification plugins if the users want to. Will think more about it and implement it in a future PR.

I didn't have any problem receiving regular push when the app is in background or closed, but silent pushes are not supported at the moment, we will add the - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler in a future release.

jcesarmobile commented 4 years ago

Just did some testing and by doing this with a custom UNUserNotificationCenterDelegate in AppDelegate the Capacitor one keeps working as expected.

func userNotificationCenter(_ center: UNUserNotificationCenter,
                              didReceive response: UNNotificationResponse,
                              withCompletionHandler completionHandler: @escaping (() -> Void)) {
    // Do your custom handling here
    let capVC = self.window?.rootViewController as? CAPBridgeViewController
    capVC?.bridge?.notificationsDelegate.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler)
  }

  func userNotificationCenter(_ center: UNUserNotificationCenter,
                              willPresent notification: UNNotification,
                              withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    // Do your custom handling here
    let capVC = self.window?.rootViewController as? CAPBridgeViewController
    capVC?.bridge?.notificationsDelegate.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler)
  }

From custom plugins is even easier as they already have the bridge object and can skip the let capVC = self.window?.rootViewController as? CAPBridgeViewController part.

rpanadero commented 4 years ago

Thank you very much @jcesarmobile

yacut commented 3 years ago

@jcesarmobile does your solution work for Capacitor v3?

ionitron-bot[bot] commented 1 year ago

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Capacitor, please create a new issue and ensure the template is fully filled out.