Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[Bug]: Push subscription listener unpredictable #1783

Open
2 of 3 tasks
hrastnik opened this issue Jan 28, 2025 · 4 comments
Open
2 of 3 tasks

[Bug]: Push subscription listener unpredictable #1783

hrastnik opened this issue Jan 28, 2025 · 4 comments

Comments

@hrastnik
Copy link

hrastnik commented Jan 28, 2025

What happened?

On version 5.2.8 (lower version are possibly also affected) on Android after calling User.pushSubscription.addEventListenter("change", callback) the callback behaves very unpredictable.

When User.pushSubscription.optIn or User.pushSubscription.optOut is called, it's expected the callback will fire with the new value. However that doesn't happen.

Steps to reproduce?

Run this on an Android device:

import React, { useRef } from "react";
import {
  Text as RNText,
  ScrollView,
  ToastAndroid,
  TouchableOpacity,
} from "react-native";
import { LogLevel, OneSignal } from "react-native-onesignal";
import type { TextProps } from "react-native";

const ONESIGNAL_APP_ID = "------API-ID-GOES-HERE-----";
const EXTERNAL_ID = "-----EXTERNAL-ID-GOES-HERE------";

const listenerMap = new Map();

function getListener<F extends undefined | ((...args: any[]) => any)>(
  name: string,
  listener: F,
): F {
  if (listenerMap.has(name)) {
    return listenerMap.get(name);
  }

  listenerMap.set(name, listener);
  return listener as any;
}

const Text = ({ style, ...props }: TextProps) => (
  <RNText style={[{ color: "black", fontSize: 14 }, style]} {...props} />
);

const Button = ({
  title,
  onPress,
}: {
  title: string;
  onPress: (() => void) | (() => Promise<void>);
}) => {
  return (
    <TouchableOpacity
      onPress={onPress}
      style={{
        justifyContent: "center",
        backgroundColor: "rgb(88, 72, 208)",
        paddingHorizontal: 4,
        paddingVertical: 4,
      }}
    >
      <Text style={{ color: "white" }}>{title}</Text>
    </TouchableOpacity>
  );
};

export const AppEntry = function AppEntry() {
  const intervalId = useRef<null | ReturnType<typeof setInterval>>(null);

  return (
    <ScrollView
      style={{ backgroundColor: "white" }}
      contentContainerStyle={{
        paddingVertical: 60,
        paddingHorizontal: 4,
        gap: 8,
      }}
    >
      <Text>Run all of the following:</Text>
      <Button
        title={[
          "setConsentRequired(false)",

          "initialize(:appId)",

          "Location.setShared(false)",
          "login(:externalID)",

          "Notif.addEventListener('permissionChange', :fn)",
          "User.addEventListener('change', :fn)",
          "User.pushSubscription.addEventListener('change', :fn)",

          "requestPermission()",
        ].join("\n")}
        onPress={async () => {
          console.log("Debug.setLogLevel(Verbose)");
          OneSignal.Debug.setLogLevel(LogLevel.Verbose);

          console.log("setConsentRequired(false)");
          OneSignal.setConsentRequired(false);

          console.log("initialize(:appId)");
          OneSignal.initialize(ONESIGNAL_APP_ID);

          console.log("Location.setShared(false)");
          OneSignal.Location.setShared(false);

          console.log("login(:externalID)");
          OneSignal.login(EXTERNAL_ID);

          console.log("Notif.addEventListener('permissionChange', :fn)");
          OneSignal.Notifications.addEventListener(
            "permissionChange",
            getListener("Notifications.permissionChange", (event) => {
              console.log("Notifications.permissionChange", event);
            }),
          );

          console.log("User.addEventListener('change', :fn)");
          OneSignal.User.addEventListener(
            "change",
            getListener("User.change", (event) => {
              console.log("User.change", event);
            }),
          );

          console.log("User.pushSubscription.addEventListener('change', :fn)");
          OneSignal.User.pushSubscription.addEventListener(
            "change",
            getListener("User.pushSubscription.change", (event) => {
              ToastAndroid.show(
                "User.pushSubscription.change event!",
                ToastAndroid.SHORT,
              );
              console.log(
                "User.pushSubscription.change",
                JSON.stringify(event),
              );
            }),
          );

          console.log("requestPermission()");
          console.log(
            "  ",
            await OneSignal.Notifications.requestPermission(false),
          );

          if (intervalId.current) {
            clearInterval(intervalId.current);
            intervalId.current = null;
          }
          intervalId.current = setInterval(() => {
            console.log("User.pushSubscription.optIn()");
            OneSignal.User.pushSubscription.optIn();
            setTimeout(() => {
              console.log("User.pushSubscription.optOut()");
              OneSignal.User.pushSubscription.optOut();
            }, 1000);
          }, 2000);
        }}
      />

      <Text>Push subscription</Text>
      <Button
        title="clearInterval(global.intervalId)"
        onPress={() => {
          if (intervalId.current) {
            clearInterval(intervalId.current);
            intervalId.current = null;
          }
        }}
      />

      <Button
        title="User.pushSubscription.optIn()"
        onPress={() => {
          console.log("User.pushSubscription.optIn()");
          OneSignal.User.pushSubscription.optIn();
        }}
      />
      <Button
        title="User.pushSubscription.optOut()"
        onPress={() => {
          console.log("User.pushSubscription.optOut()");
          OneSignal.User.pushSubscription.optOut();
        }}
      />
      <Button
        title="User.pushSubscription.addEventListener('change', :fn)"
        onPress={() => {
          console.log("User.pushSubscription.addEventListener('change', :fn)");
          OneSignal.User.pushSubscription.addEventListener(
            "change",
            getListener("User.pushSubscription.change", (event) => {
              ToastAndroid.show(
                "User.pushSubscription.change event!",
                ToastAndroid.SHORT,
              );
              console.log(
                "User.pushSubscription.change",
                JSON.stringify(event),
              );
            }),
          );
        }}
      />

      <Text>Removing event listeners</Text>

      <Button
        title="Notif.removeEventListener('permissionChange', :fn)"
        onPress={() => {
          console.log("Notif.removeEventListener('permissionChange', :fn)");
          const fn = getListener("Notifications.permissionChange", () => {});
          if (fn) {
            OneSignal.Notifications.removeEventListener(
              "foregroundWillDisplay",
              fn,
            );
          } else {
            console.log("No listener found");
          }
        }}
      />
      <Button
        title="User.removeEventListener('change', :fn)"
        onPress={() => {
          console.log("User.removeEventListener('change', :fn)");
          const fn = getListener("User.change", () => {});
          if (fn) {
            OneSignal.User.removeEventListener("change", fn);
          } else {
            console.log("No listener found");
          }
        }}
      />
      <Button
        title="User.pushSubscription.removeEventListener('change', :fn)"
        onPress={() => {
          console.log(
            "User.pushSubscription.removeEventListener('change', :fn)",
          );
          const fn = getListener("User.pushSubscription.change", () => {});
          if (fn) {
            OneSignal.User.pushSubscription.removeEventListener("change", fn);
          } else {
            console.log("No listener found");
          }
        }}
      />

      <Text>Debug</Text>

      <Button
        title="Debug.setLogLevel(Verbose)"
        onPress={() => {
          console.log("Debug.setLogLevel(Verbose)");
          OneSignal.Debug.setLogLevel(LogLevel.Verbose);
        }}
      />
      <Button
        title="Debug.setLogLevel(None)"
        onPress={() => {
          console.log("Debug.setLogLevel(None)");
          OneSignal.Debug.setLogLevel(LogLevel.None);
        }}
      />
    </ScrollView>
  );
};

Click to button at the top of the screen. And observe the logs. I'm not sure why, but sometimes it will log the pushSubscription change event successfully, and at other times, it will not.

What did you expect to happen?

When User.pushSubscription.optIn or User.pushSubscription.optOut is called, it's expected the callback will fire with the new value consistently. However that doesn't happen always.

React Native OneSignal SDK version

5.2.8

Which platform(s) are affected?

  • iOS
  • Android

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct
@nan-li
Copy link
Contributor

nan-li commented Jan 30, 2025

Hi @hrastnik, thanks for reaching out with your question.

When User.pushSubscription.optIn or User.pushSubscription.optOut is called, it's expected the callback will fire with the new value consistently. However that doesn't happen always.

The push subscription listener will fire when any properties change. I think you are asking about the optedIn property on this callback. This returns true when the app has notifications permission and optOut() is not called. If device has no notification permission, toggling between optOut() and optIn() will not change the optedIn property, as it is false in both cases due to the device itself not having native permissions.

Can you give some exact scenarios where you see the inconsistency?

@hrastnik
Copy link
Author

@nan-li The issue persists even with native permissions granted. The code I posted actually does request native permissions.

I try to track the state of 'optedInby getting the initial value usingOneSignal.User.pushSubscription.getOptedInAsync(), and then setting up a listener using OneSignal.User.pushSubscription.addEventListener('change', callback)and inside the callback, tracking the value ofevent.current.optedIn`.

However, in some cases the callback doesn't fire, so my app thinks the user never opted in.

@hrastnik
Copy link
Author

I've tried to differentiate between scenarios when the callback doesn't fire, but I can't figure it out.

After some investigation I think it might be a problem with the native operation queue, but I'm not sure.

@nan-li nan-li changed the title [Bug]: [Bug]: Push subscription listener unpredictable Feb 10, 2025
@nan-li
Copy link
Contributor

nan-li commented Feb 10, 2025

@hrastnik if you reproduce a scenario where the listener is not behaving as expected, please share verbose-level logs starting before you call optIn or optOut. It is difficult to determine what may be happening without the logs.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants