react native push notification doesn't run in background

4.1k Views Asked by At

I'm trying to implement push notifications with react native app with this plugin react-native-push-notifications. what I succeed is to get notification when app is running(in foreground), but what I'm looking to do is to get local notification when app is closed(background), not running and when I get notification to go into the app.

This is my Code -

  PushNotification.createChannel({
    channelId: "alarm-channel",
    channelName: "Alarm Channel",
    importance: 4,
    vibrate: true,
  });
};

const [selectedDate, setSelectedDate] = useState(new Date());
const handleDatePicker = (dateTime) => {
  const fireDate = dateTime;

  PushNotification.localNotificationSchedule({
    channelId: "alarm-channel",
    title: 'title',
    id: alarmNotifData.id,
    message: 'message',
    date: fireDate ,
    soundName: "Default",
    actions: ["Snooze", "Stop Reminder"],
    importance: Importance.HIGH,
    playSound: true,
    allowWhileIdle: true,
    invokeApp: false,
    priority: "high",
    timeoutAfter: 50000,
    autoCancel: false,
    vibrate: true,
    vibration: 500,
  });
};

handleDatePicker(selectedDate);
3

There are 3 best solutions below

3
berkzel On

Use background services to run your code when app is closed. For Android, react-native has Headless.js but to do that you need to write some native code. Unfortunately react-native has no documentation for IOS right now.

For Android : https://reactnative.dev/docs/headless-js-android

A library that does this for both IOS & Android : https://www.npmjs.com/package/react-native-background-actions

5
Uch On

If you're looking to implement notifications in the background. I highly recommend adopting the @notifee/react-native package.

Installation instructions

You can use React Native AppState to check whether or not you're currently in the foreground of the app or in the background.

For iOS, I personally like to use AppState.current === "inactive" since its lifecycle is always active -> inactive -> background.

For Android I use AppState.current === "background" since Android only has active -> background

// Outside of your Functional Component, Create a type called NotificationType. 
// This will make it easy for you to choose between which in app notification you send 

export enum NotificationType {
  Eating = "EATING",
  Sleeping = "SLEEPING",
}

// Notification ID is used for Android, I only need one personally
export enum NotificationID {
  DefaultID = "default",
}

Next, for the file you'll notify from, import AppState and notifee

import {AppState} from "react-native"
import notifee from "@notifee/react-native";

Within your app setup code create a state variable for appState and set up an AppState listener


  const [appState, setAppState] = useState(AppState.currentState);

  // This is used as a callback function whenever AppState changes
  const handleAppStateChange = useCallback(
    (newState: any) => {
      console.log(`AppState changed to ${newState}`);
      setAppState(newState);
    },
    []
  );

  // Initial useEffect to set things up
  useEffect(() => {
    let isActive = true;
    let appStateSubscriber: NativeEventSubscription;
    const loadNavSubscriber = async () => {
      if (isActive) {
        // Adding the listener for whether the user leaves the app
        console.log(`Adding AppState event listener`);
        appStateSubscriber = AppState.addEventListener("change", 
        handleAppStateChange);
      }
    };
    loadNavSubscriber();
    
    // It's always good practice to ask the user for notification permissions
    const getNotificationPermissions = async () => {
      if (isActive) {
        const settings = await notifee.requestPermission();
      }
    };
    getNotificationPermissions();
    return () => {
      isActive = false;
      if (appStateSubscriber) appStateSubscriber.remove();
    };
  }, []);

Now Create a handleNotification method. This works for both Android and iOS


  const handlePushNotification = useCallback(
    async (
      enabled: boolean, // I simply pass in the appState state variable or AppState.current
      notificationID: string,
      notificationType: NotificationType
    ) => {
      // Display a notification
      if (!enabled) {
        return;
      }
      const channelId = await notifee.createChannel({
        id: notificationID,
        name: "APP_NAME_TO_DISPLAY",
        visibility: AndroidVisibility.PUBLIC,
        importance: AndroidImportance.HIGH,
        // This adds vibration but I personally don't like vibration
        // vibration: false,
        // vibrationPattern: [100, 200, 100, 200],
      });

      if (notificationType === NotificationType.EATING) {
        await notifee.displayNotification({
          title: "Eating = YUMMY",
          body: `I love to eat food`,
          ios: { categoryId: "eating_category" },
          android: {
            channelId,
            smallIcon: "ic_icon", //read docs if you want an icon 
            importance: AndroidImportance.HIGH,
            pressAction: {
              id: "androidOpenApp",
              launchActivity: "default",
              launchActivityFlags: [AndroidLaunchActivityFlag.SINGLE_TOP],
            },
            actions: [
              {
                title: "Stop Eating", //this will show if you long press the notification. Read docs to set up actions 
                pressAction: {
                  id: "stop-eating",
                },
              },
            ],
          },
        });
      } else if (notificationType === NotificationType.SLEEP) {
        await notifee.displayNotification({
          title: "SLEEP - Time for bed",
          body: `It's time to go to sleep!`,
          ios: { categoryId: "sleep_category" },
          android: {
            channelId,
            smallIcon: "ic_icon",
            importance: AndroidImportance.HIGH,
            pressAction: {
              id: "androidOpenApp",
              launchActivity: "default",
              launchActivityFlags: [AndroidLaunchActivityFlag.SINGLE_TOP],
            },
// You may have the same actions or different ones depending on your app
            actions: [
              {
                title: "Stop Sleeping",
                pressAction: {
                  id: "stop-sleeping",
                },
              },
            ],
          },
        });
      } 
    },
    [... whatever deps you need]
  );

Then, somewhere in your code you can say.

if (nextCustomerActivity === "sleeping") {
  // Notify them that they need to go to sleep
   handlePushNotification(
// Only notify if the app is in the background
      Platform.OS === "ios" && appState === "inactive" || Platform.OS === "android" && appState === "background", 
    NotificationID.DefaultID,
      NoticationType.Sleeping
    );
} else if (nextCustomerActivity === "eating) {
  // notify them they need to start eating
handlePushNotification(
      Platform.OS === "ios" && appState === "inactive" || Platform.OS === "android" && appState === "background", // Only notify if the app is in the background
    NotificationID.DefaultID,
      NoticationType.Eating
    );
} else {
  // no need to notify
}

In android if you're experiencing that a new instance of your app is getting created when you open up a notification, make sure you check your AndroidManifest.xml. This took me hours to fix in the past.

android:windowSoftInputMode="adjustPan"
        android:launchMode="singleTask"
        android:exported="true"

the whole activity in question for AndroidManifest.xml

    <activity
      android:name=".MainActivity"
      android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
      android:windowSoftInputMode="adjustPan"
        android:launchMode="singleTask"
        android:exported="true">
     <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="YOUR_APP_NAME" />
    </intent-filter>
    </activity>

Good luck!

0
Mohammad H On

The issue you're experiencing might be due to missing dependencies in your android\app\build.gradle file, my issue fixed by adding this settings:

dependencies {
    // ...
    implementation 'com.google.firebase:firebase-analytics:17.3.0'
    implementation platform('com.google.firebase:firebase-bom:31.0.0')
    implementation 'com.google.firebase:firebase-functions'
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.google.firebase:firebase-iid:21.1.0'
    // ...
}

Firebase provides several services such as Firebase Functions, Firebase Messaging and Firebase Instance IDs, which are used by push notification systems. Make sure to include these dependencies


In addition, react-native-push-notification requires a notification channel to be specified for your push notifications. This can be done by adding specific meta-data tags inside the application tag in your AndroidManifest.xml file :

<application>
  <meta-data 
    android:name="com.dieam.reactnativepushnotification.notification_channel_name"
    android:value="PUSH_NOTIFICATION_CHANNEL" />
  <meta-data
    android:name="com.dieam.reactnativepushnotification.default_notification_channel_id"
    android:value="PUSH_NOTIFICATION_CHANNEL" />
  <meta-data 
    android:name="com.dieam.reactnativepushnotification.notification_channel_description"
    android:value="Channel for push notification events" />
</application>

These meta-data tags define the name, ID, and description of your notification channel. You can learn more about notification channels from the Android Developers guide.


Furthermore, I've heard that it's best to provide a delay to the open link in order to ensure that the application is up and capable of handling it. However, I'm not sure if this is true, so I modified my onNotification in PushNotification.configure to add a delay to it if the app is in the foreground:

onNotification: async function (notification) {
  if (notification.userInteraction) {
    const { link = null } = notification?.data || {};
    if (!link) return
    if (notification.foreground === false) {
      // The app was in the background or closed, delay the link opening
      setTimeout(() =>
        Linking.openURL(link).catch(e => console.log(e))
        , 1000);
    } else
      Linking.openURL(link).catch(e => console.log(e));
  }
},

Please take note that I have saved the link in the notification data; you are free to modify it according to your preferences.