havesource / cordova-plugin-push

Register and receive push notifications
MIT License
148 stars 283 forks source link

[Solved with workaround] Push notifications are not received on IOS device after app starts if subscribed to topic. #181

Open armandoxxx opened 2 years ago

armandoxxx commented 2 years ago

Bug Report

IOS notifications on subscribed topics are not received by IOS device on first application startup. You have to unsubscribe in subscribe to the topic again to make it work.

Expected Behaviour

When debuging application you start it up, close it and then push FCM message through. Notification is not received or shown on device.

Actual Behaviour

On first start of app when debuging, nothing happens when FCM push message is sent although log messages say that app is subscribed to topic. If you open app, unsubscribe from topic and subscribe to it again, all newly sent push messages appear from now on.

Reproduce Scenario (including but not limited to)

I've created this project to test it. https://github.com/armandoxxx/AngularWithCordova But use your own google-service.json and GoogleService-Info,pllist settings Checkout project, read README cause there are some strange things to consider before build. go into MobileApp folder. Do what readme says you have to do to start app. cordova build ios run app with XCode check logs

Steps to Reproduce

Platform and Version (eg. Android 5.0 or iOS 9.2.1)

Ipad with latest IOS 15.x

cordova info Printout

Cordova Packages:

    cli: 11.0.0

        common: 4.0.2

        create: 4.0.0

        lib: 11.0.0

            common: 4.0.2

            fetch: 3.0.1

            serve: 4.0.0

Project Installed Platforms:

    ios: 6.2.0

Project Installed Plugins:

    @havesource/cordova-plugin-push: 3.0.0

    cordova-plugin-camera: 6.0.0

Environment:

    OS: macOS Monterey 12.3.1 (21E258) (darwin 21.4.0) x64

    Node: v12.22.8

    npm: 6.14.15

ios Environment:

    xcodebuild:

Xcode 13.3

Build version 13E113

Project Setting Files:

    config.xml:

<?xml version='1.0' encoding='utf-8'?>

<widget id="com.dropchop.mobile" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">

    <name>MobileApp</name>

    <description>Sample Apache Cordova App</description>

    <author email="[dev@cordova.apache.org](mailto:dev@cordova.apache.org)" href="https://cordova.apache.org/">

        Apache Cordova Team

    </author>

    <content src="index.html" />

    <access origin="*" />

    <allow-navigation href="*" />

    <allow-intent href="http://*/*" />

    <allow-intent href="https://*/*" />

    <platform name="android">

        <resource-file src="google-services.json" target="app/google-services.json" />

    </platform>

    <platform name="ios">

        <resource-file src="GoogleService-Info.plist" />

        <allow-intent href="itms:*" />

        <allow-intent href="itms-apps:*" />

        <preference name="AllowUntrustedCerts"  value="true" />

        <preference name="InterceptRemoteRequests" value="all" />

        <preference name="allowFileAccessFromFileURLs" value="true" />

        <preference name="allowUniversalAccessFromFileURLs" value="true" />

        <edit-config target="NSCameraUsageDescription" file="*-Info.plist" mode="merge">

            <string>need camera access to take pictures</string>

        </edit-config>

        <edit-config target="NSPhotoLibraryUsageDescription" file="*-Info.plist" mode="merge">

            <string>need photo library access to get pictures from there</string>

        </edit-config>

        <edit-config target="NSPhotoLibraryAddUsageDescription" file="*-Info.plist" mode="merge">

            <string>need photo library access to save pictures there</string>

        </edit-config>

    </platform>

</widget>

    package.json:

--- Start of Cordova JSON Snippet ---

{

  "plugins": {

    "cordova-plugin-camera": {

      "ANDROIDX_CORE_VERSION": "1.6.+"

    },

    "@havesource/cordova-plugin-push": {

      "IOS_FIREBASE_MESSAGING_VERSION": "~> 6.32.2",

      "ANDROIDX_CORE_VERSION": "1.6.+",

      "FCM_VERSION": "18.+"

    }

  },

  "platforms": [

    "android"

  ]

}

--- End of Cordova JSON Snippet ---

armandoota@clippitamine MobileApp % 

    package.json:

--- Start of Cordova JSON Snippet ---

{

  "plugins": {

    "cordova-plugin-camera": {

      "ANDROIDX_CORE_VERSION": "1.6.+"

    },

    "@havesource/cordova-plugin-push": {

      "IOS_FIREBASE_MESSAGING_VERSION": "~> 6.32.2",

      "ANDROIDX_CORE_VERSION": "1.6.+",

      "FCM_VERSION": "18.+"

    }

  },

  "platforms": [

    "android"

  ]

}

--- End of Cordova JSON Snippet ---

Sample Push Data Payload

FCM v1

https://fcm.googleapis.com/v1/projects/{{project id}}/messages:send
{   
    "message": {
        "topic":"user_topic",
         "notification": {
            "title": "title generic",
            "body": "body generic"
        },
        "android": {
            "notification": {
                "title": "title Android",
                "body": "body andorid"
            }
        },
        "apns": {
            "payload": {
                "aps" : {
                    "alert" : {
                        "title" : "title IOS",
                        "body" : "Body IOS"
                    }
                }
            }
        },
        "data": {
            "article": "some article"
        }   
    }
}

FCM legacy

https://fcm.googleapis.com/fcm/send
{
    "to":"/topics/user_topic",
    "notification":{
      "title":"Portugal vs. Denmark IOS",
      "body":"great match!"
    }
}

Sample Code that illustrates the problem

Maybe there is something wrong with how Push plugin is initialized in Angular app app.component.ts

export class AppComponent implements OnInit, OnDestroy {

  title = 'AngularCordovaNg';
  subscribedTopics: string[] = [];
  data: string = '';
  currentTime: Date = new Date();

  private push: any = undefined;

  //work flags
  private eventsInitialized: boolean = false;
  private subscribing: boolean = false;
  private subscribedOnRegister: boolean = false;

  private topicSubscriptionInitTimer:any = undefined;

  private disableSubscriptions: Subject<void> = new Subject<void>();

  private registration: Subject<void> = new Subject<void>();
  private registrationsSource: Subject<void> = new Subject<void>();

  private afterRegistrationsEvent: Observable<void> = this.registrationsSource.asObservable();

  constructor(private cdRef: ChangeDetectorRef, private broadcastService: BroadcastService){

  }

  ngOnInit(): void {
    this.initEvents();
    this.initPush();
  }

  ngOnDestroy(): void {
    this.disableSubscriptions.next();
    this.disableNotificationEvents();
  }

  initPush() {
    console.log('Initializing push plugin.');
    let config = {
      android: {
        vibrate: true,
        clearNotifications: true
      },
      ios: {
        fcmSandbox: true,
        alert: true,
        badge: true,
        sound: true
      }
    }
    this.push = PushNotification.init(config);
    this.enableNotificationEvents();

    PushNotification.hasPermission(
      () => {
        console.log("Notification permission granted");
        //this.subscribeToTopic('user_topic'); //commented out to try and replace with registration observable.
      },
      () => {
        console.log("not permitted to receive notifications!");
      }
    );
  }

  initEvents() {
    this.broadcastService.pushSubscriptionsEvent.pipe(takeUntil(this.disableSubscriptions)).subscribe( (subscriptionData: PushSubscriptionData) => {
      console.log('Got subscription event data [%o]', subscriptionData);
      if (subscriptionData.action == 'subscribe') {
        this.subscribeToTopic(subscriptionData.topicName);
      } else if (subscriptionData.action == 'unsubscribe') {
        this.unSubscribeFromTopic(subscriptionData.topicName);
      }
    });
    //try and simulate to subscribe to topic after last registration event is done
    this.afterRegistrationsEvent.pipe(takeUntil(this.disableSubscriptions)).subscribe(
      () => {

          if (this.topicSubscriptionInitTimer != undefined) {
            clearTimeout(this.topicSubscriptionInitTimer);
          }
          let me = this;
          this.topicSubscriptionInitTimer = setTimeout( () => {
            console.log('Executing timer');
            me.subscribeToTopic('user_topic')
          }, 2000);
      }
    );
  }

  private subscribeToTopic(topicName: string): void {
    if (this.subscribedTopics.indexOf(topicName) !== -1) {
      console.log('Already subscribed to topic [%s]', topicName);
      return;
    }
    if (this.subscribing) {
      console.log('Already subscribing to topic [%s]', topicName);
      return;
    }
    console.log("Will subscribe to topic: [%s]", topicName);
    this.subscribing = true;
    this.push.subscribe(topicName,
      () => {
        console.log('Subscribed to [%s]', topicName);
        this.addTopic(topicName);
        this.subscribing = false;
      },
      () => {
        console.log("Cannot subscribe to [%s]", topicName);
        this.removeTopic(topicName);
        this.subscribing = false;
      }
    );
  }

  private unSubscribeFromTopic(topicName: string): void {
    if (this.subscribedTopics.indexOf(topicName) === -1) {
      console.log('Not subscribed to topic [%s]', topicName);
      return;
    }
    this.push.unsubscribe(
      topicName,
      () => {
        console.log('Successfully unsubscribed from topic [%s]', topicName);
        this.removeTopic(topicName);
      },
      (e: any) => {
        console.log('Cannot unsubscribe from topic [%s] error [%o]', topicName, e);
      }
    );
  }

  private onNotificationEvent(data: any) {
    console.log("Got notification data: %o", data);
    this.data = JSON.stringify(data);
    this.currentTime = new Date();
    this.cdRef.detectChanges();
  }

  private onNotificationError(data: any) {
    console.log("Got notification error data: %o", data);
  }

  private onRegistration(data: any) {
    console.log("Got registration data: %o", data);
    this.registrationsSource.next();
  }

  private enableNotificationEvents() {
    if (this.eventsInitialized) {
      console.log('Events already initialized');
      return;
    }
    this.push.on('registration', this.onRegistration.bind(this));
    this.push.on('notification', this.onNotificationEvent.bind(this));
    this.push.on('error', this.onNotificationError.bind(this));
    this.eventsInitialized = true;
  }

  private disableNotificationEvents() {
    if (!this.eventsInitialized) {
      console.log('Events not initialized!');
      return;
    }
    this.push.off('registration', this.onRegistration.bind(this));
    this.push.off('notification', this.onNotificationEvent.bind(this));
    this.push.off('error', this.onNotificationError.bind(this));
    console.log("Events disabled");
    this.eventsInitialized = false;
    this.subscribedOnRegister = false;
  }

  private addTopic(topicName: string): void {
    this.subscribedTopics.push(topicName);
    console.log('added topic [%s]', topicName);
    this.cdRef.detectChanges();
  }

  private removeTopic(topicName: string): void {
    if (this.subscribedTopics.indexOf(topicName) !== -1) {
      this.subscribedTopics.splice(this.subscribedTopics.indexOf(topicName), 1);
      console.log('removed topic [%s]', topicName);
    }
    this.cdRef.detectChanges();
  }

  private isTopicSubscribed(topicName: string): void {

  }
}

Logs taken while reproducing problem

2022-04-08 12:20:57.895041+0200 MobileApp[23851:695703] application first launch: remove badge icon number
2022-04-08 12:20:57.900412+0200 MobileApp[23851:695703] PushPlugin skip clear badge
2022-04-08 12:20:58.333791+0200 MobileApp[23851:695703] Initializing push plugin.
2022-04-08 12:20:58.391716+0200 MobileApp[23851:695703] Notification permission granted
2022-04-08 12:20:58.391940+0200 MobileApp[23851:695703] Will subscribe to topic: [user_topic]
2022-04-08 12:20:58.392058+0200 MobileApp[23851:695703] subscribe from topic: user_topic
2022-04-08 12:20:58.393011+0200 MobileApp[23851:695703] Successfully subscribe to topic user_topic
2022-04-08 12:20:58.399980+0200 MobileApp[23851:695703] Push Plugin VoIP missing or false
2022-04-08 12:20:58.400269+0200 MobileApp[23851:695719] Push Plugin register called
2022-04-08 12:20:58.400317+0200 MobileApp[23851:695719] PushPlugin.register: setting badge to false
2022-04-08 12:20:58.400355+0200 MobileApp[23851:695719] PushPlugin.register: clear badge is set to 0
2022-04-08 12:20:58.400393+0200 MobileApp[23851:695719] PushPlugin.register: better button setup
2022-04-08 12:20:58.404092+0200 MobileApp[23851:695719] FCM Sender ID 1079642262479
2022-04-08 12:20:58.404187+0200 MobileApp[23851:695719] Using FCM Notification
2022-04-08 12:20:58.404246+0200 MobileApp[23851:695719] Using FCM Sandbox
2022-04-08 12:20:58.527295+0200 MobileApp[23851:695703] Subscribed to [user_topic]
2022-04-08 12:20:58.527501+0200 MobileApp[23851:695703] added topic [user_topic]

2022-04-08 12:20:58.527295+0200 MobileApp[23851:695703] Subscribed to [user_topic] this says that app is subscribed to topic.

So either this is a bug or my init process is wrong. yes i'm initializing everything on deviceready main.ts

const bootstrap = () => {
  platformBrowserDynamic().bootstrapModule(AppModule);
};

if (typeof window['cordova'] !== 'undefined') {
  document.addEventListener('deviceready', () => {
    bootstrap();
  }, false);
} else {
  bootstrap();
}
armandoxxx commented 2 years ago

I successfuly subscribed to topic on both flatforms. I had to implement timeout which resets every time registered event is called. After all register events are done subscribtion to topic can be executed. Check angular app.compontent.ts for the latest code. Maybe update to docs: Call subscribe() method after all register events are complete. I dunno why this is not explanied in the docs.