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]: OneSignal#onWillDisplayNotification getting called as many times as app gets hot restarted while debugging. #763

Open
3 tasks done
luis901101 opened this issue Oct 10, 2023 · 37 comments

Comments

@luis901101
Copy link
Contributor

luis901101 commented Oct 10, 2023

What happened?

The callback for _handleMethod(MethodCall call) function on OneSignalNotifications class is getting called as many times as the app get's Hot Restarted, so listeners gets called several times times during debugging.

Steps to reproduce?

No need to add any listener, just put a break point on first line in `Future<Null> _handleMethod(MethodCall call) async` on `OneSignalNotifications`, then:
1. Run app in debug mode.
2. Initialize OneSignal
3. Do Hot Restart X times
4. Send one push notification to your debugging device
5. Note the function getting called X times.

What did you expect to happen?

The callback should be getting called just one time by notification event on each app hot restart.

OneSignal Flutter SDK version

5.0.3

Which platform(s) are affected?

  • iOS
  • Android

Relevant log output

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct
@luis901101
Copy link
Contributor Author

With OneSignal#onClickNotification happens similar, in this case as it's a manual event you have to add a listener for that event, so when calling addClickListener(...) it's triggering a _channel.invokeMethod("OneSignal#addNativeClickListener");, so every time you hot-restart your app and add your click listener the same channel method will be invoked and then you will be called back several times, one by each hot-restart.

The problem seems that on the native code side a List of listeners is kept, and this list remains the same on each hot-restart, so on each OneSignal initialization or adding click listener, the native list of listeners increases.

@emawby
Copy link
Contributor

emawby commented Oct 11, 2023

@luis901101 Thank you for reporting we will investigate. In the meantime the corresponding removeListener most likely needs to be called to remove the old listener or you could try not adding the second listeners on hot reload

@luis901101
Copy link
Contributor Author

Sure, the remove listener should be properly called, but note in my description above I'm talking about Hot Restart not Hot Reload, and also about the OneSignal#onWillDisplayNotification it doesn't matter the listener, I mean, it will be called X times depending on the amount of X times you Hot Restart the app, no matter if you added a listener or not, because it seems that on native implementation side the listeners list are being added on every OneSignal.initialize().

@luis901101
Copy link
Contributor Author

luis901101 commented Oct 11, 2023

The call to OneSignal.Notifications.addForegroundWillDisplayListener(listener); is being handled only on dart side, I mean the listener you add there will be added only to: _willDisplayListeners.add(listener);, later when some notification is received the Future<Null> _handleMethod(MethodCall call) is called and _willDisplayListeners will be checked to notify each listener... but no matter if you added listeners to _willDisplayListeners or not... the handler will be called X times depending on the amount of times the app has been Hot Restarted.
Then if you have added 1 listener, but you have Hot Restarted your app 10 times your listener will be called 10 times...

@emawby
Copy link
Contributor

emawby commented Oct 11, 2023

@luis901101 I see thank you for investigating! We will work on a fix for this

@alainjr10
Copy link

With OneSignal#onClickNotification happens similar, in this case as it's a manual event you have to add a listener for that event, so when calling addClickListener(...) it's triggering a _channel.invokeMethod("OneSignal#addNativeClickListener");, so every time you hot-restart your app and add your click listener the same channel method will be invoked and then you will be called back several times, one by each hot-restart.

The problem seems that on the native code side a List of listeners is kept, and this list remains the same on each hot-restart, so on each OneSignal initialization or adding click listener, the native list of listeners increases.

I been stuck on this issue for a few days now, and i couldn't quite reproduce it until now. Have you got a temporary workaround for this? @luis901101

@luis901101
Copy link
Contributor Author

Nope, I just avoid onesignal initialization during debug unless I need to test something related to notifications which is sporadical.

@temcewen
Copy link

+1 I am also experiencing this issue

@meroo36
Copy link

meroo36 commented Nov 15, 2023

+1

3 similar comments
@ologunB
Copy link

ologunB commented Nov 24, 2023

+1

@Jeonguk-ninehire
Copy link

+1

@frankenten13
Copy link

+1

@singlapriyanka315
Copy link

singlapriyanka315 commented Dec 15, 2023

facing same issue

@csnguyenskg
Copy link

same here

@vr0707
Copy link

vr0707 commented Jan 5, 2024

Please help any one!!! i used one signal 5.0.4 version, adclicklistener lisent multiple times. how to handle this ?

@arhamsc
Copy link

arhamsc commented Jan 7, 2024

Is there any ETA to when this issue is going to get resolved? Also will this affect the "release" version of flutter app or only debug?

@bugrevealingbme
Copy link

Same

@nan-li
Copy link
Contributor

nan-li commented Jan 25, 2024

EDIT: I meant Hot Restart in this comment

Hi everyone, thanks for your reports.

The OneSignal-Flutter-SDK does support adding multiple listeners of each type. After investigating, this seems like a Flutter Hot Reload issue, and I am not sure how the SDK can work around it without impacting real usage.

When you run a Hot Reload, it doesn't cold start the app but still reruns your initialization code, which is why your listeners are added again. Just put a breakpoint or print statement in the place where you are adding your listener and you will see the Hot Reload does call your code again to add the listeners.

Because it is not a cold start, all of the SDK's data remains in memory and it cannot distinguish between a Hot Reload vs. normal app flow or backgrounding, etc. The SDK cannot distinguish that you are adding a listener for real or Hot Reload is adding the listener.

If you have suggestions how to work around this for easier debugging, please let us know. It doesn't seem the SDK can make this fix.

@luis901101
Copy link
Contributor Author

Hi @nan-li and thanks for your feedback, but note that in my first comment I state Hot Restarts not Hot Reloads

The callback for _handleMethod(MethodCall call) function on OneSignalNotifications class is getting called as many times as the app get's Hot Restarted, so listeners gets called several times times during debugging.

So the way I see it, if the app is Hot Restarted the OneSignal.initialize(appId); will be called again and at this point the plugin should reset any previous state, I mean all listeners should be cleaned wether at dart side or native side.

@PetrKubes97
Copy link

I'm encountering this issue as well and I think resetting all listeners on native side when calling OneSignal.initialize as @luis901101 suggested would be the best behavior.

@nan-li
Copy link
Contributor

nan-li commented Jan 26, 2024

Hi @nan-li and thanks for your feedback, but note that in my first comment I state Hot Restarts not Hot Reloads

The callback for _handleMethod(MethodCall call) function on OneSignalNotifications class is getting called as many times as the app get's Hot Restarted, so listeners gets called several times times during debugging.

So the way I see it, if the app is Hot Restarted the OneSignal.initialize(appId); will be called again and at this point the plugin should reset any previous state, I mean all listeners should be cleaned wether at dart side or native side.

Hi @luis901101,

I apologize, I misspoke in my comment, I meant to say Hot Restart as that is the flow I tested. I disagree that calling OneSignal.initialize(appId) should reset the SDK. Imagine this setup scenario:

    OneSignal.Debug.setLogLevel(OSLogLevel.verbose);
    OneSignal.consentRequired(_requireConsent);

    OneSignal.Notifications.addForegroundWillDisplayListener((event) {
        print('Foreground display listener triggered');
    });

    OneSignal.Notifications.addClickListener((event) {
        print('NOTIFICATION CLICK LISTENER CALLED WITH EVENT: $event');
    });

    OneSignal.initialize("YOUR_APP_ID");

There are some methods that could be called before OneSignal.initialize. All of the setup work done by the developer would be erased. This would be a significant behavior change and lead to unexpected behavior, just for the sake of Hot Reloads. If the SDK is already initialized and another call to initialize it is made, I would expect a no-op.

@luis901101
Copy link
Contributor Author

Hi @nan-li thanks again for you feedback and your tests, oki then we are on the same page with the Hot Restart (by the way you mentioned Hot Reloads again at the end of your comment 😉)

Agree, resetting the whole plugin state is not ideal, but resetting only the listeners I think it makes sense and in fact only the listener are the ones with the problems mentioned in this thread so no need to reset anything else.

From your sample code I understand the setLogLevel and consentRequired could be called before initialization but I'm not sure the listeners make sense to be added before initialization, I mean, is there any case in which some of these listeners would be called if the plugin has not been initialized?

@PetrKubes97
Copy link

Thanks for the reply. I understand that changing the behavior in such way is not ideal, even though I myself wouldn't think of doing anything before initialization.

What about a method that clears all listeners in both Flutter and native side? Keeping track of the function references for their removal is annoying anyway.

@nan-li
Copy link
Contributor

nan-li commented Jan 29, 2024

Hi @luis901101 oops yes you are right, I just can't stop saying Hot Reloads 😆.

The thing is we would like to avoid any "gotchas" that SDK users have to watch for. If they have complex code and already called initialize but then later call it again, we shouldn't produce unexpected behavior. This is same if they do make some add listener calls before calling initialize.

I also recall on our previous major release with the player-based model, some people's notification opened handlers didn't trigger early enough when the app is cold started from the open event. We had suggested to some users to add the opened handler as early as possible.

In addition, clearing listeners on initialize will differ from all the other OneSignal SDKs (native Android, native iOS, react native, etc), which is not preferable behavior either. There has not been issues with listeners being added on other SDKs since Hot Restarts is a Flutter specific issue.

@PetrKubes97 You should manage the removal and addition of listeners when debugging only. Perhaps call the remove method before the add methods during debugging with Hot Restarts.

@PetrKubes97
Copy link

@nan-li sorry if this comment is inaccurate, I'm trying to remember the code and typing it on a phone 😅.

From what I remember, the remove method only removes the listener from the list of listeners in flutter with native listeners still being registered. What happens then is that there are two native listeners, which call the flutter listener twice (even though there is only one in the list)

@luis901101
Copy link
Contributor Author

luis901101 commented Jan 29, 2024

Ok let's break this into parts:

  1. IMPORTANT: the issue identified in this thread has nothing to do with SDK users adding/removing listeners.
  2. Back to my initial comment I refer to the _handleMethod(MethodCall call) function getting called as many times as the app get Hot Restarted, so no matter if the SDK user has added listers or not.
  3. The SDK users you are trying to protect by not doing any change on the SDK initialization so they don't experience unexpected behavior, those users are being affected anyway by this issue which by the way it is an unexpected behavior, (I'm not the only one reporting it 🤷‍♂️).
  4. Ok, Hot Restart is a specific Flutter feature but this plugin is for Flutter, so we expect the plugin's behavior is according to the Flutter dev tools, Hot Restarting a debugging app is a valid use case, so the Plugin should work fine with hot restarts.

Follow this simple test case carefully (so far I only tested it on iOS):

  1. Create a simple Flutter OneSignal app project.
  2. Make sure you initialize OneSignal but DO NOT ADD ANY LISTENERS.
  3. Run your app in debug mode
  4. Put a break point inside the Future<Null> _handleMethod(MethodCall call){...} located in notifications.dart class on OneSignal plugin, specifically on the Line 133:
    Future<Null> _handleMethod(MethodCall call) async {
    if (call.method == 'OneSignal#onClickNotification') {
    for (var listener in _clickListeners) {
    listener(
    OSNotificationClickEvent(call.arguments.cast<String, dynamic>()));
    }
    } else if (call.method == 'OneSignal#onWillDisplayNotification') {
    for (var listener in _willDisplayListeners) {
    listener(OSNotificationWillDisplayEvent(
    call.arguments.cast<String, dynamic>()));
    }
    var event = OSNotificationWillDisplayEvent(
    call.arguments.cast<String, dynamic>());
    _channel.invokeMethod("OneSignal#proceedWithWillDisplay",
    {'notificationId': event.notification.notificationId});
    } else if (call.method == 'OneSignal#onNotificationPermissionDidChange') {
    this.onNotificationPermissionDidChange(call.arguments["permission"]);
    }
    return null;
    }
  5. From OneSignal dashboard send a test notification to your device. (Create a test segment to avoid sending these notifications to real users)
  6. Hot Restart your app and repeat from point 5.

If you hot restart your app 5 times, when you send the test notification to your device, you will see the _handleMethod(MethodCall call) get's called 6 times, in fact when this several calls happens the plugin shows an ERROR in the logs, something like:

2024-01-29 22:11:25.899960+0000 Test App[4657:128228] ERROR: OSNotificationWillDisplayEvent.notification.display cannot be called due to timing out or notification was already displayed.

@temcewen
Copy link

@nan-li Hi Nan,

First, I'd like to say that I appreciate the work you and the OneSignal team are doing!

I'd like to address your comment when you said, 'we would like to avoid any "gotchas" that SDK users have to watch for' and then proceeded to explain the reasoning behind allowing behaviors such as duplicate initialization calls or allowing hooks to be set before initialization is called.

Would it not be a "gotcha" that people will probably expect that calling initialize sometime after an app has started should re-initialize the app and clear any existing hooks? A call to "initialize" something implies that you have to call it before anything else. It implies that you're dealing with a state machine and it implies that after a call to "initialize" the state machine, you will be working with a "clean state".

By allowing the API to be used in erroneous ways like disregarding when initialized is called, ignoring duplicate initialize calls, etc, you actually significantly worsen the developer experience. For example, if someone is unintentionally calling initialize, don't you think they would want to know? Don't you think they would want to know, "Hey, you're calling this function twice when you only need to call it once just FYI."

This is akin to what JavaScript does when it allows users to do var x = "0" + 1 and then returns a value and WHO KNOWS what the type will be. It's antithetical to safe object oriented programming. Abstractions should use exceptions and/or warnings to indicate to developers when they are used incorrectly rather than just allowing the developer to do it and then ASSUMING some intention on behalf of the dev that obviously can't be known without explicitly asking.

I apologize for being a little dogmatic, but I would really appreciate some concrete reasons behind why initialize shouldn't clear the existing OneSignal state. Perhaps it could give a warning when it does so?

What if there were a happy medium where initialize has an optional parameter bool resetState = false?

@PetrKubes97
Copy link

PetrKubes97 commented Jan 30, 2024

Maybe a quick fix:
Just move the _clickHandlerRegistered = true; to iOS and Android part of the SDK? But tbh random flags like this seem a bit hacky.

I think it could be done more declaratively, checking whether listeners are registered rather than checking some flag. The current bug is caused by the mismatch between the flags state and the registered listeners state.

@AchmadSyarippudin
Copy link

same issue on onesignal 5.2, this anybody have solved this problem ?

@sannykhan3777
Copy link

sannykhan3777 commented Jun 25, 2024

Have the same issue.
I'm navigating to another screen on click of notification but get 10+ same screens under the navigation stack because of listener calling itself multiple times.

Onesignal SDK = onesignal_flutter: ^5.1.0
Flutter SDK = 3.16.3

OneSignal.Notifications.addClickListener((event) { String? screen = event.notification.additionalData!["screen"]; if(screen != null){ onesignalNavigatorKey.currentState!.pushNamed(RoutesName.orders_home_screen); } });

@zvikarp
Copy link
Contributor

zvikarp commented Jun 26, 2024

Following, and hoping for a proper fix from OneSignal.

In the meantime we have added a debounce, here is a basic template, if it helps anyone here:


  void _onNotificationOpenedWrapper(OSNotificationClickEvent event) {
    // the debounce is add to avoid multiple calls to the same event
    // this is due to a bug in the onesignal plugin that calls the event
    // multiple times.
    if (_notificationDebounce?.isActive ?? false) return;
    _notificationDebounce = Timer(
      const Duration(seconds: 1),
      () => _notificationDebounce = null,
    );
    _onNotificationOpened(event);
  }

  void _onNotificationOpened(OSNotificationClickEvent event) {
    // do something
  }

  void listenForNotificationOpened() {
    OneSignal.Notifications.addClickListener(_onNotificationOpenedWrapper);
  }

@jt274
Copy link

jt274 commented Jul 31, 2024

I appear to be experiencing this issue in release mode as well. A notification that pushes a navigation route gets pushed twice when opened.

@Ibrar-Atrule
Copy link

meantime we have added a debounce, here is a basic template, if it helps anyone here:

Have the same issue. I'm navigating to another screen on click of notification but get 10+ same screens under the navigation stack because of listener calling itself multiple times.

Onesignal SDK = onesignal_flutter: ^5.1.0 Flutter SDK = 3.16.3

OneSignal.Notifications.addClickListener((event) { String? screen = event.notification.additionalData!["screen"]; if(screen != null){ onesignalNavigatorKey.currentState!.pushNamed(RoutesName.orders_home_screen); } });

Did you find the solution? I am having the same issue but I didn't find a solution of why it is doing like that?

@luis901101
Copy link
Contributor Author

meantime we have added a debounce, here is a basic template, if it helps anyone here:

Have the same issue. I'm navigating to another screen on click of notification but get 10+ same screens under the navigation stack because of listener calling itself multiple times.
Onesignal SDK = onesignal_flutter: ^5.1.0 Flutter SDK = 3.16.3
OneSignal.Notifications.addClickListener((event) { String? screen = event.notification.additionalData!["screen"]; if(screen != null){ onesignalNavigatorKey.currentState!.pushNamed(RoutesName.orders_home_screen); } });

Did you find the solution? I am having the same issue but I didn't find a solution of why it is doing like that?

Hi @Ibrar-Atrule the "why the plugin is doing like that" in my opinion is because wrong handling of listeners initialization on native side, I mention it in my previous comments... until someone make a PR with the fix, what you can do is to handle the issue with a debounce strategy like the one described in this comment #763 (comment), for the addForegroundWillDisplayListener and addClickListener basically the idea is to ignore consecutive events when they occur in a short period of time.

Here some example as an idea...

  void startListeners() {
    final onReceiveListener = _handleOneSignalReceivedNotification;
    OneSignal.Notifications.removeForegroundWillDisplayListener(onReceiveListener);
    OneSignal.Notifications.addForegroundWillDisplayListener(onReceiveListener);
    
    final onClickListener = _handleOneSignalClickNotification;
    OneSignal.Notifications.removeClickListener(onClickListener);
    OneSignal.Notifications.addClickListener(onClickListener);
  }

  static int lastNotificationReceivedTime = 0;
  void _handleOneSignalReceivedNotification(OSNotificationWillDisplayEvent event) {
    try {
      //FIXME: remove this code when OneSignal fixes the issue: https://github.com/OneSignal/OneSignal-Flutter-SDK/issues/763
      final now = DateTime.now().microsecondsSinceEpoch;
      if (now - lastNotificationReceivedTime < 1000) {
        event.preventDefault();
        return;
      }
      // If function gets here the notification will be displayed
    } catch (e) {
      print(e);
    }
  }

  static int lastNotificationClickedTime = 0;
  void _handleOneSignalClickNotification(OSNotificationClickEvent event) {
    try {
      //FIXME: remove this code when OneSignal fixes the issue: https://github.com/OneSignal/OneSignal-Flutter-SDK/issues/763
      final now = DateTime.now().microsecondsSinceEpoch;
      if (now - lastNotificationClickedTime < 1000) return;
      final Map<String, dynamic>? payloadAdditionalData = event.notification.additionalData;
      openConentFromNotification(payloadAdditionalData);
    } catch (e) {
      print(e);
    }
  }

@mrasityilmaz
Copy link

Issue Description

I've encountered a recurring problem with the OneSignal Flutter SDK where listeners are triggered repeatedly and increasingly more frequently with each execution. This issue was first reported back in 2023, and as of September 2024, it still persists.

Proposed Solution

To resolve this, I implemented a solution that tracks the last processed notification ID and prevents the listener from executing multiple times for the same notification. Here’s how I achieved it step by step:

  1. Define a Variable to Store the Last Notification ID:

    I created a static variable to keep track of the last notification's ID:

    static String? _lastNotificationId;
  2. Add a Foreground Listener:

    I added a listener using OneSignal.Notifications.addForegroundWillDisplayListener to handle foreground notifications:

    OneSignal.Notifications.addForegroundWillDisplayListener((event) {
      // Check if the notification ID matches the last processed ID
      if (_lastNotificationId == event.notification.notificationId) {
        return;
      } else {
        // Update the last notification ID and proceed
        _lastNotificationId = event.notification.notificationId;
        print('notification addForegroundWillDisplayListener');
      }
    });
    
  3. Prevent Duplicate Processing:
    By comparing the current notification's ID with the last processed ID, the listener prevents duplicate processing for the same notification.

  4. Handling Multiple Notifications:
    If your application expects multiple notifications to arrive simultaneously, you can store the notification IDs in a list instead of a single variable. This way, you can manage and track multiple notifications efficiently.

@Ibrar-Atrule
Copy link

Issue Description

I've encountered a recurring problem with the OneSignal Flutter SDK where listeners are triggered repeatedly and increasingly more frequently with each execution. This issue was first reported back in 2023, and as of September 2024, it still persists.

Proposed Solution

To resolve this, I implemented a solution that tracks the last processed notification ID and prevents the listener from executing multiple times for the same notification. Here’s how I achieved it step by step:

  1. Define a Variable to Store the Last Notification ID:
    I created a static variable to keep track of the last notification's ID:
    static String? _lastNotificationId;
  2. Add a Foreground Listener:
    I added a listener using OneSignal.Notifications.addForegroundWillDisplayListener to handle foreground notifications:
    OneSignal.Notifications.addForegroundWillDisplayListener((event) {
      // Check if the notification ID matches the last processed ID
      if (_lastNotificationId == event.notification.notificationId) {
        return;
      } else {
        // Update the last notification ID and proceed
        _lastNotificationId = event.notification.notificationId;
        print('notification addForegroundWillDisplayListener');
      }
    });
  3. Prevent Duplicate Processing:
    By comparing the current notification's ID with the last processed ID, the listener prevents duplicate processing for the same notification.
  4. Handling Multiple Notifications:
    If your application expects multiple notifications to arrive simultaneously, you can store the notification IDs in a list instead of a single variable. This way, you can manage and track multiple notifications efficiently.

I also have done it through its notification ID, I have created a set when I have stored the notification ID, that's how one notification can't be appeared multiple times, and it worked for me.

@sumonsheikhdev
Copy link

Issue Description

I've encountered a recurring problem with the OneSignal Flutter SDK where listeners are triggered repeatedly and increasingly more frequently with each execution. This issue was first reported back in 2023, and as of September 2024, it still persists.

Proposed Solution

To resolve this, I implemented a solution that tracks the last processed notification ID and prevents the listener from executing multiple times for the same notification. Here’s how I achieved it step by step:

1. **Define a Variable to Store the Last Notification ID:**
   I created a static variable to keep track of the last notification's ID:
   ```dart
   static String? _lastNotificationId;
   ```

2. **Add a Foreground Listener:**
   I added a listener using `OneSignal.Notifications.addForegroundWillDisplayListener` to handle foreground notifications:
   ```dart
   OneSignal.Notifications.addForegroundWillDisplayListener((event) {
     // Check if the notification ID matches the last processed ID
     if (_lastNotificationId == event.notification.notificationId) {
       return;
     } else {
       // Update the last notification ID and proceed
       _lastNotificationId = event.notification.notificationId;
       print('notification addForegroundWillDisplayListener');
     }
   });
   ```

3. **Prevent Duplicate Processing:**
   By comparing the current notification's ID with the last processed ID, the listener prevents duplicate processing for the same notification.

4. **Handling Multiple Notifications:**
   If your application expects multiple notifications to arrive simultaneously, you can store the notification IDs in a list instead of a single variable. This way, you can manage and track multiple notifications efficiently.

Yes working . Thanks lot

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

No branches or pull requests