tinycreative / react-native-intercom

React Native wrapper for Intercom.io
MIT License
407 stars 282 forks source link

Push notifications on android won't work with FCM #336

Closed nikitawolfik closed 4 years ago

nikitawolfik commented 4 years ago

React-native: 0.59.10 React-native-intercom: 13.2.0

I've researched quite a few issues, followed a lot of suggestions and here I am:

  1. Either I include

    <service android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
    </service>

    in my manifest, so push notifications from FCM work both in foreground and background, but intercom pushes don't get shown, while are received in logcat

    12-12 12:35:30.746  4798  5639 D RNFMessagingService: onMessageReceived event received
    12-12 12:35:30.752  4798  4798 D RNFirebaseMessaging: Received new message
  2. Or I remove the earlier code and only leave this

    <service
        android:name="com.robinpowered.react.Intercom.IntercomIntentService"
        android:exported="false">
      <intent-filter
          android:priority="999">
        <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
      </intent-filter>
    </service>
    <receiver
        android:name="io.intercom.android.sdk.push.IntercomPushBroadcastReceiver"
        tools:replace="android:exported"
        android:exported="true" />

then Intercom pushes work both fg and bg, while FCM only works bg.

Obviously, these two configs are conflicting, but I don't understand how to make it work.

Some other configuration:

MainMessagingService.java

package com.kubia.app;
import io.invertase.firebase.messaging.*;
import android.content.Intent;
import android.content.Context;
import io.intercom.android.sdk.push.IntercomPushClient;
import io.invertase.firebase.messaging.RNFirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import android.util.Log;
import java.util.Map;

public class MainMessagingService extends RNFirebaseMessagingService {
    private static final String TAG = "MainMessagingService";
    private final IntercomPushClient intercomPushClient = new IntercomPushClient();

    @Override public void onNewToken(String refreshedToken) {
    intercomPushClient.sendTokenToIntercom(getApplication(), refreshedToken);
    //DO HOST LOGIC HERE
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Map message = remoteMessage.getData();

        if (intercomPushClient.isIntercomPush(message)) {
            Log.d(TAG, "Intercom message received");
            intercomPushClient.handlePush(getApplication(), message);
        } else {
            super.onMessageReceived(remoteMessage);
        }
    }
}

AndroidManifest.xml services

<application
    android:name=".MainApplication"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:allowBackup="false"
    android:theme="@style/AppTheme"
    xmlns:tools="http://schemas.android.com/tools">
    <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
    <service
      android:name=".MainMessagingService"
      android:enabled="true"
      android:exported="true">
        <intent-filter>
          <action android:name="com.google.firebase.MESSAGING_EVENT" />
          <data android:host="kubia.page.link" android:scheme="https"/>
          <data android:host="i.kubia.com" android:scheme="https"/>
          </intent-filter>
    </service>
    <service
        android:name="com.robinpowered.react.Intercom.IntercomIntentService"
        android:exported="false">
      <intent-filter
          android:priority="999">
        <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
      </intent-filter>
    </service>
    <receiver
        android:name="io.intercom.android.sdk.push.IntercomPushBroadcastReceiver"
        tools:replace="android:exported"
        android:exported="true" />
    <service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />
    <service android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
    </service>
    <service android:name="io.invertase.firebase.messaging.RNFirebaseInstanceIdService">
      <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
      </intent-filter>
    </service>
...
</application>

build.gradle

...
    implementation 'io.intercom.android:intercom-sdk:6.+'

NotificationsManager.js

const intercomChannel = new firebase.notifications.Android.Channel(
      'intercom_chat_replies_channel',
      'Intercom Replies Channel',
      firebase.notifications.Android.Importance.Max,
    ).setDescription('Channel for intercom replies');

    firebase.notifications().android.createChannel(intercomChannel);

It looks like the overriden onMessageReceived is never gets called in case (1) and I can't find out why

nikitawolfik commented 4 years ago

NVM, there was a configuration issue, fixed it

StijnCoolen commented 4 years ago

@NikitaNeganov what did you do to fix this?

nikitawolfik commented 4 years ago

@StijnCoolen actually, I had quite a few things misconfigured.

  1. I had my deeplink and dynamic link schemes inside <application> instead of <activity> intent-filters.

So it was like this

<application
    android:name=".MainApplication"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:allowBackup="false"
    android:theme="@style/AppTheme"
    xmlns:tools="http://schemas.android.com/tools">
    <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
    <service
      android:name=".MainMessagingService"
      android:enabled="true"
      android:exported="true">
        <intent-filter>
          <action android:name="com.google.firebase.MESSAGING_EVENT" />
          <data android:host="my-deeplink-host" android:scheme="https"/>
          <data android:host="my-dynamiclink-host" android:scheme="https"/>
          </intent-filter>
    </service>
    <service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />
    <service android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
    </service>
    <service android:name="io.invertase.firebase.messaging.RNFirebaseInstanceIdService">
      <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
      </intent-filter>
    </service>
...

while it should have been here

<activity
      android:name=".MainActivity"
      android:label="@string/app_name"
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <intent-filter android:label="MyLabel">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="my-scheme" android:host="help" />
        <data android:host="my-deeplink-host" android:scheme="https"/>
        <data android:host="my-dynamiclink-host" android:scheme="https"/>
        <data android:scheme="https" android:host="kubia.com" />
      </intent-filter>
    </activity>
  1. I didn't need these two services since they were overridden in my MainMessagingService.java, as per #208
    <service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />
    <service android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>

So in the end my AndroidManifest.xml looked like this

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.my-app-name.app">
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  <uses-permission android:name="android.permission.USE_FINGERPRINT" />
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.VIBRATE"/>
  <uses-permission android:name="android.permission.READ_CONTACTS" />
  <uses-permission android:name="android.permission.READ_PROFILE" />
  <application
    android:name=".MainApplication"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:allowBackup="false"
    android:theme="@style/AppTheme"
    xmlns:tools="http://schemas.android.com/tools">
    <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
    <service
      android:name=".MainMessagingService"
      android:enabled="true"
      android:exported="true">
        <intent-filter>
          <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
    <service android:name="io.invertase.firebase.messaging.RNFirebaseInstanceIdService">
      <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
      </intent-filter>
    </service>
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
      android:launchMode="singleTask"
      android:screenOrientation="portrait"
      android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
      android:windowSoftInputMode="adjustPan">
      <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <intent-filter android:label="My-Label">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="my-deeplink-scheme" android:host="help" />
        <data android:host="my-dynamiclink-scheme" android:scheme="https"/>
        <data android:host="my-dynamiclink-scheme-2" android:scheme="https"/>
        <data android:scheme="https" android:host="my-deeplink-host" />
      </intent-filter>
    </activity>
    <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
  </application>
</manifest>

MainMessagingService.java

I didn't have onNewToken method initially and I'm still not sure if I need this, but since everything works I just let it go.

package com.kubia.app;
import io.invertase.firebase.messaging.*;
import android.content.Intent;
import android.content.Context;
import io.intercom.android.sdk.push.IntercomPushClient;
import io.invertase.firebase.messaging.RNFirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import android.util.Log;
import java.util.Map;

public class MainMessagingService extends RNFirebaseMessagingService {
    private static final String TAG = "MainMessagingService";
    private final IntercomPushClient intercomPushClient = new IntercomPushClient();

    @Override public void onNewToken(String refreshedToken) {
    intercomPushClient.sendTokenToIntercom(getApplication(), refreshedToken);
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Map message = remoteMessage.getData();

        if (intercomPushClient.isIntercomPush(message)) {
            Log.d(TAG, "Intercom message received");
            intercomPushClient.handlePush(getApplication(), message);
        } else {
            super.onMessageReceived(remoteMessage);
        }
    }
}

I also added these lines to NotificationsManager.js right in the same place where I created my default channel for FCM

const intercomChannel = new firebase.notifications.Android.Channel(
      'intercom_chat_replies_channel',
      'Intercom Replies Channel',
      firebase.notifications.Android.Importance.Max,
    ).setDescription('Channel for intercom replies');

    firebase.notifications().android.createChannel(intercomChannel);

That should be about everything I changed, hope it helps

StijnCoolen commented 4 years ago

@NikitaNeganov thanks, you mention you removed <service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" /> because it is already added in MainMessagingService.java, I don't see anything handling background messages there. Also, can you share your NotificationsManager.js?

nikitawolfik commented 4 years ago

@StijnCoolen yeah, I didn't actually notice that service missing since PNs still work in fg, bg and with the app killed. Probably that is covered by RNFirebaseMessagingService, but I'm not sure

Here's NotitifactionsManager.js

import React from 'react';
import PropTypes from 'prop-types';
import { Platform } from 'react-native';
import firebase from 'react-native-firebase';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withNavigation, NavigationActions } from 'react-navigation';
import Intercom from 'react-native-intercom';

import { pushNotifications } from 'redux/actions';

const CHANNEL = 'default';

const NotificationsManager = ({
  registerToken,
  children,
  navigation: {
    reset,
    navigate,
  },
}) => {
  const localNotificationListener = React.useRef();
  const notificationOpenListener = React.useRef();
  const onTokenRefreshListener = React.useRef();

  const runFlow = async () => {
    try {
      await firebase.messaging().requestPermission();
      const enabled = await firebase.messaging().hasPermission();

      if (enabled) {
        const fcmToken = await firebase.messaging().getToken();

        if (fcmToken) {
          if (Platform.OS === 'android') {
            Intercom.sendTokenToIntercom(fcmToken);
          }

          registerToken({
            device_token: fcmToken,
          });
        }
      }
    } catch (e) {
      // pass
    }
  };

  const receiveForegroundPN = (n) => {
    const notification = new firebase.notifications.Notification()
      .setNotificationId(n.notificationId)
      .setTitle(n.title)
      .setSubtitle(n.subtitle)
      .setBody(n.body)
      .setData(n.data);

    if (Platform.OS === 'android') {
      notification
        .android.setChannelId(CHANNEL)
        .android.setSmallIcon('ic_launcher');
    }
    firebase.notifications().displayNotification(notification);
  };

  const openNotification = ({ notification }) => {
    const { data } = notification;
    // do something here
  };

  const replaceStack = (stack, index) => {
    reset([
      NavigationActions.navigate({
        routeName: 'Dashboard',
      }),
      ...stack,
    ], index);
  };

  React.useEffect(() => {
    const channel = new firebase.notifications.Android.Channel(
      CHANNEL,
      'Bank Notifications',
      firebase.notifications.Android.Importance.Max,
    ).setDescription('Notifications about transactions, one time passwords, etc.');

    const intercomChannel = new firebase.notifications.Android.Channel(
      'intercom_chat_replies_channel',
      'Intercom Replies Channel',
      firebase.notifications.Android.Importance.Max,
    ).setDescription('Channel for intercom replies');

    firebase.notifications().android.createChannel(channel);
    firebase.notifications().android.createChannel(intercomChannel);

    localNotificationListener.current = firebase.notifications().onNotification(receiveForegroundPN);
    notificationOpenListener.current = firebase.notifications().onNotificationOpened(openNotification);

    onTokenRefreshListener.current = firebase.messaging().onTokenRefresh((fcmToken) => {
      registerToken({
        device_token: fcmToken,
      });
    });

    runFlow();

    return () => {
      localNotificationListener.current();
      notificationOpenListener.current();
      onTokenRefreshListener.current();
    };
  }, []);

  return children || null;
};

NotificationsManager.propTypes = {
  registerToken: PropTypes.func.isRequired,
  children: PropTypes.node,
  navigation: PropTypes.shape({
    reset: PropTypes.func.isRequired,
    navigate: PropTypes.func.isRequired,
  }).isRequired,
};

export default compose(
  withNavigation,
  connect(null, {
    registerToken: pushNotifications.registerDeviceToken,
  }),
)(NotificationsManager);