Notificare / notificare-sdk-react-native

Notificare's Official React Native Module
MIT License
6 stars 0 forks source link

🐞: [Android]: Notificare.onUrlOpened does not work on cold boot #68

Closed VNDRN closed 5 months ago

VNDRN commented 5 months ago

Is there an existing issue for this?

Describe the bug

When sending a deeplink in a notification to an android device that does not have the app open, the notification opens the app, but Notificare.onUrlOpened or any other callback does not get triggered.

This means we cannot execute the deeplink embedded in the notification.

I've followed the setup from the documentation

Steps to reproduce

  1. add deeplink support in an android app.
  2. close the app.
  3. send a notification with a deeplink
  4. app opens, but deeplink does not get caught.

Expected behaviour

Notificare.onUrlOpened gets triggered (or another method from NotificarePush or NotificarePushUI)

Relevant log output

No response

Library version

3.8.0

Operating system

Android 10, Android 12

React Native version

0.73.6

Smartphone model

Xiaomi, Oneplus

Additional context

I'm using deeplinking with the recommended setup of React Navigation. The deep link from the notification should be handled in the getInitialURL function like this:

export async function getInitialURL() {
  // Check if app was opened from a deep link
  const url = await Linking.getInitialURL();
  console.log('initialURL', { url });
  if (url !== null) {
    return url;
  }
  Notificare.onUrlOpened(notificationDeeplinkUrl => {
    console.log('onUrlOpened: ', notificationDeeplinkUrl);
    if (!notificationDeeplinkUrl) {
      return;
    }

    if (isValidString(notificationDeeplinkUrl)) {
      return notificationDeeplinkUrl;
    }

    // If no deeplink provided, open the fallbacknotificationDeeplinkUrl.
    return FALLBACK_DEEPLINK;
  });
}
Y-Smirnov commented 5 months ago

Hello @VNDRN,

When sending the deep link inside the notification, the app will be opened from the notification and not the deep link directly.

The flow will look like the following:

Although it may seem like an extended process, the execution is fast. Therefore, it is important that you set up those listeners as soon as possible in your application.

Additionally, you have a condition before setting the Notificare.onUrlOpened listener which may result in the listener being ignored. This listener will handle all deep links, including the initial one. You should set Notificare.onUrlOpened either way and you may use it without Linking.getInitialURL().

VNDRN commented 5 months ago

Hi @Y-Smirnov , thanks for looking into this. As you've suggested, I've added this as soon as possible in my app.

NotificarePush.onNotificationOpened(async notification => {
    console.log('Notification opened:', notification);
    await NotificarePushUI.presentNotification(notification);
  });

Then in my getInitialURL function as the recommended setup of React Navigation uses, I do this:

export async function getInitialURL() {
   Notificare.onUrlOpened(notificationDeeplinkUrl => {
    console.log('onUrlOpened: ', notificationDeeplinkUrl);
    if (!notificationDeeplinkUrl) {
      return;
    }

    if (isValidString(notificationDeeplinkUrl)) {
      return notificationDeeplinkUrl;
    }

    // If no deeplink provided, open the fallbacknotificationDeeplinkUrl.
    return FALLBACK_DEEPLINK;
  });

However, this still has the same result: the notification get's presented, but on cold boot, the deeplink is not triggered by onUrlOpened...

I've tried to add it to the sample project but I'm struggling to get it running on my device

Y-Smirnov commented 5 months ago

Hi @VNDRN

Similarly to NotificarePush.onNotificationOpened,Notificare.onUrlOpened should also be placed as soon as possible in your app.

The onUrlOpened() and other event functions are meant to be executed when you mount a component. They return an EmitterSubscription, meaning you need to clean it up when the component unmounts. We are not too familiar with the React Navigation library, but what you’re referencing points to a getInitialURL() function that is meant to provide the initial deep link. Your app will never open with a deep link when you’re dealing with deep link notifications. As I stated, the original intent is to handle the notification open, which later resolves to the deep link after you present it. At that point the onUrlOpened() will fire. However, the way you setup the listener, it’s probably not being kept by the React Navigation getInitialURL() function.

All you need to do is set up the listener similarly you do with NotificarePush.onNotificationOpened. You can also investigate the subscribe(listener) in React Navigation since it seems to be their option for processing non-initial deep links, which is the case for deep links triggered by a notification. This problem you’re experiencing seem to be related to your integration of React Navigation and not a bug in our library.

I hope this helps.

VNDRN commented 5 months ago

Hi @Y-Smirnov, As you suggested, this is the hook useLaunchNotificare, which I call in my App.tsx. I think this should work according to your explanation? However, I'm still facing the same issue:

Am I overlooking something?

This is the code:

// useLaunchNotificare.ts

const useLaunchNotificare = () => {
  const enabled = useFeatureFlag('notificare'); // returns true

  const [isNotificareReady, setIsNotificareReady] = useState(false);

  useEffect(
    function launch() {
      (async () => {
        if (!enabled) {
          return;
        }
        // This will make sure that the app will receive notifications when it's in the foreground on iOS.
        // On Android, this is handled by the notificare library
        const availableOptions = ['alert', 'badge', 'sound', 'banner', 'list'];
        await NotificarePush.setPresentationOptions(availableOptions);
        await Notificare.launch();
      })();
    },
    [enabled],
  );

  useEffect(function setupListeners() {
    const subscriptions = [
      Notificare.onReady(async () => {
        await checkRemoteNotifications();

        setIsNotificareReady(true);
      }),
      Notificare.onDeviceRegistered(device => {
        logger.info('Registered notificare device:', device.id);
      }),
      NotificarePush.onNotificationOpened(async notification => {
        await NotificarePushUI.presentNotification(notification);
      }),
      Notificare.onUrlOpened(url => {
        logger.info('=== URL OPENED ===');
        logger.info(JSON.stringify(url, null, 2));
        AsyncStorage.setItem('debugShizzle', JSON.stringify(url));
      }),
    ];

    return () => subscriptions.forEach(s => s.remove());
  }, []);

  return isNotificareReady;
};

/**
 * For existing users that accepted notifications before but aren't enabled in notificare yet, we need to enable remote notifications
 */
async function checkRemoteNotifications() {
  const { status } = await checkNotifications();
  const hasRemoteNotificationsEnabled = await NotificarePush.hasRemoteNotificationsEnabled();

  if (
    hasRemoteNotificationsEnabled ||
    (isPermissionGranted(status) && !hasRemoteNotificationsEnabled)
  ) {
    await NotificarePush.enableRemoteNotifications();
  }
}
<!-- android/app/src/main/AndroidManifest.xml -->

<!-- notificare intent-->
      <intent-filter>
        <action android:name="re.notifica.intent.action.RemoteMessageOpened" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
      <!-- Deeplink intent filter -->
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="mydeeplinkscheme" />
      </intent-filter>
hpinhal commented 5 months ago

Hi @VNDRN,

At a glance, your code looks correct and the onUrlOpened should be executing. However, I cannot reproduce what you're describing. Please find my example below.

function App(): React.JSX.Element {
  useEffect(() => {
    Notificare.launch()
      .then(() => console.log('Launched.'))
      .catch(err => console.log(err));
  }, []);

  useEffect(() => {
    const subscriptions = [
      Notificare.onReady(async () => {
        console.log('Ready');

        // TODO: request push permission before enabling remote notifications.
        await NotificarePush.enableRemoteNotifications();
      }),
      NotificarePush.onNotificationOpened(async notification => {
        console.log('Notification opened');
        await NotificarePushUI.presentNotification(notification);
      }),
      Notificare.onUrlOpened(url => {
        console.log(`Url opened: ${url}`);
      }),
    ];

    return () => subscriptions.forEach(s => s.remove());
  }, []);

  return (
    <>
      ...
    </>
  );
}

Which results in the following log output when opening the notification from a cold start.

 BUNDLE  ./index.js 

 LOG  Running "AwesomeProject" with {"rootTag":11}
 LOG  Notification opened
 LOG  Launched.
 LOG  Url opened: re.notifica.sample.app.dev://notificare.com/inbox
 LOG  Ready

This example is running the latest version of React Native (0.74.0).

You may want to test your application in release mode to prevent the cold start from having to load the javascript bundle.

Since we cannot reproduce the issue you're describing, please provide a small application where we can see the problem occurring. Should you need assistance specific to your app, it's best to contact our support channel via the Notificare Dashboard or at support@notifica.re.

VNDRN commented 5 months ago

hi @hpinhal, Sorry for the late reply. I've managed to get it working in the end. It was indeed solved by initialising the listeners sooner.

It was strange to see that some listeners did get initialised while others didn't. This also makes it difficult to integrate with authentication, navigation etc. which all have to be initialised as fast as possible as well 😄

Thanks for helping me out!