expo: NotificationResponseReceivedListener not called when app is killed

Summary

I ran into problems while developing an app that has to respond to push notifications. I based this app on the expo notification snack that is found in the documentation.

When trying out my app, the expo notification service works fine when an app is in the foreground or background, but does not trigger when the app is killed. I use the addnotificationresponsereceivedlistenerlistener method to handle notifications when the app is killed.

I found a lot of issues about this problem that have already been closed. Unfortunately, the solutions/pull request in these issues don’t solve the problem on my system. I therefore think this might be a bug that was introduced in version 42 of the expo SDK.

Related issues

Managed or bare workflow? If you have ios/ or android/ directories in your project, the answer is bare!

managed

What platform(s) does this occur on?

Android

SDK Version (managed workflow only)

42.0.1

Environment

Expo CLI 4.10.0 environment info:
    System:
      OS: Linux 5.11 Ubuntu 20.04.2 LTS (Focal Fossa)
      Shell: 5.0.17 - /bin/bash
    Binaries:
      Node: 14.17.1 - ~/.nvm/versions/node/v14.17.1/bin/node
      npm: 6.14.13 - ~/.nvm/versions/node/v14.17.1/bin/npm
    IDEs:
      Android Studio: AI-203.7717.56.2031.7583922
    npmPackages:
      expo: ~42.0.1 => 42.0.3
      react: 16.13.1 => 16.13.1
      react-dom: 16.13.1 => 16.13.1
      react-native: https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz => 0.63.2
      react-native-web: ~0.13.12 => 0.13.18
    npmGlobalPackages:
      expo-cli: 4.10.0
    Expo Workflow: managed

Reproducible demo or steps to reproduce from a blank project

Development mode

  1. Clone the example project or create a new expo project using the push notification snack in the expo-notifications documentation.
  2. Install the NPM dependencies.
  3. Run the app on an android emulator or a real android device.
  4. Kill the app while keeping the expo go app open and send a push notification using the push notification tool.
  5. Click the notification.
  6. See that the alert is not triggered.

Production mode

  1. Clone the example project or create a new expo project using the push notification snack in the expo-notifications documentation.
  2. Setup your google FCM credentials (see the expo documentation).
  3. Build the project .apk using the eas build -p android --profile preview command.
  4. Install the app on your device using the adb tool.
  5. Open the app.
  6. Close the app.
  7. Send a notification using the push notification tool.
  8. Click the notification.
  9. See that the alert is not triggered.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 16
  • Comments: 163 (37 by maintainers)

Most upvoted comments

@mgscreativa @lukagabadze this is my current top priority. We are taking an existing app that is already on the Play Store, and adding FCMv1 notifications and instrumenting the expo-notifications package, so we can do some realistic testing. We will provide updates here when we make progress.

I’ve been following this issue for a while, and I see more and more solutions posted. I want to take a moment to express what I want from Expo Notifications:

  • There should be a CRYSTAL CLEAR and OFFICIALLY DOCUMENTED way to get this working WITHOUT FUSS. The happy path should be EASY and CLEAR.
  • Following this documented path, a notification should open an android app from the killed state.
  • In the case of Android, you should be able to send a notification using the structure documented in Firebase and have it work. I believe I’ve seen several comments here that state you have to use an unusual message structure for it to work. If you really must structure the message in a special way, that needs to be called out loud and clear in the documentation, and maybe include an explanation of why its that way.

It really frustrates me that it is this much of a struggle to get something so fundamental to the app experience to work. This feature is table stakes for an app. I had to give up using Expo Notifications on Android on the last app I wrote and rely on the Firebase API directly.

still an issue on Android even with initializing addNotificationResponseReceivedListener before any other react code

There seems to a be a lot of confusion in this thread, understandably.

@Codelica To clarify things for people reading this (as of Expo SDK 46):

  • Expo Go - notifications when the app is backgrounded, foregrounded and killed are all received, but the response forNotifications.addNotificationResponseReceivedListener() for tapped notifications when the app is killed will be null. This is a known limitation of using Expo Go.
  • iOS - for EAS builds: notifications when the app is backgrounded, foregrounded or killed are all received and the response from Notifications.addNotificationResponseReceivedListener() when a user taps on a notification is always valid.
  • Android - for EAS builds: notifications when the app is backgrounded, foregrounded or killed are all received but the response from Notifications.addNotificationResponseReceivedListener() will be null if the user taps a notification when the app is killed.

As many have stated in this thread, for getting Android notifications working when the app is killed, you need to move Notifications.addNotificationResponseReceivedListener() to the top-level namespace out of the component tree. For most people, this means App.js.

The example that @jackkinsella shared works fine: https://github.com/expo/expo/issues/14078#issuecomment-1041294084 . Just adapt it as necessary to work with your app’s design.

We have tested this on iOS/Android SDK 46 using EAS builds for our production apps and it works fine. The docs could use with being updated to acknowledge this issue, since its been around for ages, and obviously still confuses the shit out of everyone 🙃

Thanks for letting me know! For now, please rely on the workaround I shared above:

  const lastNotificationResponse = Notifications.useLastNotificationResponse();

  React.useEffect(() => {
    if (lastNotificationResponse) {
      alert("response");
    }
  }, [lastNotificationResponse]);

@douglowder has recently started working on expo-notifications, you can expect him to be responsive on issues like this in the near future as he ramps up

Hm, I’m really not sure what’s happening here…

If I run the app with expo run:android --variant release, press the button to trigger a local notification, kill the app, tap the notification, the alert IS NOT fired. (Same is the case for eas build -p android)

But if I run the app with expo run:android --variant release, attach the Android Studio debugger, press the button to trigger a local notification, kill the app (thus unattaching the debugger), tap the notification, the alert IS fired. Very weird indeed

However- to fix this for now I would suggest switching over to useLastNotificationResponse. Using the following code, the alert is shown even when tapping a notification while the app is killed (tested locally and on eas build):

  const lastNotificationResponse = Notifications.useLastNotificationResponse();

  React.useEffect(() => {
    if (lastNotificationResponse) {
      alert("response");
    }
  }, [lastNotificationResponse]);

For those who are still struggling with this, here’s what I had to do to finally get it working fine :

  • Upgrade to Expo SDK44 and latest expo-notifications module
  • Made sure my firebase notification sent out my Rails backend API server was well formatted, like this :
notification = {
  // As stated by @a1anyip it needs to be "data" root key and not "notification"
  data: {
    notification_id: self.id,
    icon: 'my_android_drawable_notification_icon_id',   
    title: "Notification title",
    message: "Notification message",
  },
}
fcm.send_with_notification_key(token, notification)
  • Use “useLastNotificationResponse” method and make sure you move it to the very first thing you do when the app is opening - for me it was in my App.js file, first called hook
  • When hook is called, I saved the notification into my local reducer and then catch it in my notification components to redirect where I need to

Hope it helps anyone - as I spend days on this 🤦🏻 - if anyone needs help for this, contact me ! @davonrails on Twitter or https://david.fabreguette.fr/

In my case, the issue is resolved by following the official payload spec when pushing notifications. I used to send push notifications with everything put under notification key rather than data key required by Expo. Now the response received listener works when the app is in background or killed.

@rickstaa @cruzach I am also experiencing this odd behavior on sdk 42, except I am in the bare workflow. To be more clear only see addNotificationResponseReceivedListener triggered when the app is in the foreground or backgrounded state but not the killed state.

So the reason behind this is that the native event is being sent before the actual listener is available. This is why the useLastNotificationResponse hook was created, but I suppose it would be easier to have the listener be patched without the need for more user code.

One potential fix that I’ve tested is to manually trigger the emitter after adding it with the result of getLastNotificationResponseAsync if it’s not null. To try out this fix, I’ve created a gist containing the diff you should apply to expo-notifications in node_modules (if you’ve used patch-package before, you can just download this file directly and place it in your patches directory). I haven’t tested this extremely thoroughly yet, so I would use caution before pushing this out to production code, but if you can try it and let me know how it works for you, that would be helpful!

I’ve the same issue after upgrading to SDK 43, bare workflow, iOS only, Android works great for me.

Notifications.addNotificationResponseReceivedListener is executed only when the app is in foreground or background when you tapped a notification. When the app is killed, Notifications.addNotificationResponseReceivedListener is not executed after tap on a notification.

expo: 43.0.1
expo-notifications: 0.13.3

Thanks for letting me know! For now, please rely on the workaround I shared above:

  const lastNotificationResponse = Notifications.useLastNotificationResponse();

  React.useEffect(() => {
    if (lastNotificationResponse) {
      alert("response");
    }
  }, [lastNotificationResponse]);

This doesn’t work either.

@cruzach As the root cause is known is there any update about the fix?

I am using something like this (and took a good few ideas from this blog post here)

const androidNotificationStack = [];
let androidNotificationListener = null;

const enableAndroidNotificationListener = () => {
  if (Platform.OS !== "android") {
    return;
  }

  androidNotificationListener =
    Notifications.addNotificationResponseReceivedListener(
      ({ notification }) => {
        androidNotificationStack.push(notification);
      }
    );
};

enableAndroidNotificationListener();

const disableAndroidNotificationListener = () => {
  androidNotificationListener?.remove();
};

export const getAndroidNotificationFromStack = () =>
  androidNotificationStack.shift();

export const androidNotificationStackLength = () =>
  androidNotificationStack.length;

export const SomeComponent = () => {

  const onNotificationReceipt = async (notification) => {
    handlePushNotificationAction(notification.request.content.data);
  };

  useEffect(() => {
    while (androidNotificationStackLength() > 0) {
      onNotificationReceipt(getAndroidNotificationFromStack());
    }
    // We disable this listener as soon as possible so as not
    // to double process the same push notification with
    // the "normal" listeners below.
    disableAndroidNotificationListener();
   ...
   }
}

I can confirm what @a1anyip @dfabreguette-ap have said.

My issue was that I was using Customer.io’s recommended notification format, which has both a notification and data key.

Once I removed the notification key entirely and just sent a top-level data key, it started listening correctly.

Details for anyone else struggling:

"expo": "^45.0.0",
"expo-notifications": "~0.15.2",

Example payload:

{
  "data": {
    "title": "Test title",
    "message": "Test message"
  }
}

My listener is also not top-level or outside of the hierarchy. I have it inside of the subscribe method of React Navigation’s linking config.

  // Listen for deep links and push notifications
  subscribe(listener) {
    // Listen to incoming links from deep linking
    const onReceiveURL = ({ url }: { url: string }) => listener(url);
    Linking.addEventListener('url', onReceiveURL);

    // Listen to push notifications as well
    const subscription = Notifications.addNotificationResponseReceivedListener((response) => {
      /** @ts-ignore Extract the link from the push notification */
      const url = response?.notification?.request?.trigger?.remoteMessage?.data?.link as
        | string
        | undefined;
      console.log('Found link', url);

      // Let react navigation handle the URL
      if (url) listener(url);
    });

    // Clean up the event listeners
    return () => {
      Linking.removeEventListener('url', onReceiveURL);
      subscription.remove();
    };
  }

this is still an issue on Expo v44 & expo-notification 0.14. The useLastNotificationResponse workaround doesn’t work.

Please share a link to a GH repo that exhibits that behavior- it working on Android, but not on iOS. I’ve reproduced the issue on Android already so I’m fairly confident it exists

@mgscreativa thanks for that update! I can confirm toggling between legacy and FCM V1 does change notification behavior. Going back to legacy has fixed the confusing issues I was seeing with our production SDK 49 app (that includes a workaround “fix”). This seems very odd as I swear I tested this right after moving to FCM V1, so perhaps(?) it is some recent change. For me it acts differently in other ways also under FCM V1 now – our expo-notifications icon isn’t used and banners don’t appear under Android. Moving back to legacy both of those also act as expected. Very odd and a little sad that there are now two issues to contend with. Hopefully it gets addressed before the legacy API cutoff date hits. 😬

Also experiencing this word for word on FCM V1, if the app is foregrounded then all is well, if it’s not the notification still gets delivered but it not longer has the icon that was set in app.json and no longer shows a popup. It only shows up in the status bar. (Android only, EAS Build, SDK 49)

Investigating this further: I am able to consistently reproduce the issue by switching between the Legacy FCM (no issues at all) and FCM v1 (has the issue) Something else I also noticed is that if I do setNotificationChannelAsync(“namehere”, …) and I specify that channel in the payload the Legacy FCM always deliveres to that channel, whereas the FCM v1 delivers to that channel only if the app is in foreground, but will deliver to the expo default “Miscellaneous” channel if backgrounded Copying this here from discord as requested, @douglowder

@mgscreativa thanks for that update!

I can confirm toggling between legacy and FCM V1 does change notification behavior. Going back to legacy has fixed the confusing issues I was seeing with our production SDK 49 app (that includes a workaround “fix”). This seems very odd as I swear I tested this right after moving to FCM V1, so perhaps(?) it is some recent change. For me it acts differently in other ways also under FCM V1 now – our expo-notifications icon isn’t used and banners don’t appear under Android. Moving back to legacy both of those also act as expected. Very odd and a little sad that there are now two issues to contend with.

Hopefully it gets addressed before the legacy API cutoff date hits. 😬

Also experiencing this word for word on FCM V1, if the app is foregrounded then all is well, if it’s not the notification still gets delivered but it not longer has the icon that was set in app.json and no longer shows a popup. It only shows up in the status bar. (Android only, EAS Build, SDK 49)

I can confirm this in the demo project here https://github.com/mgscreativa/expo-notifications-test.

app in foreground Android: working but it triggers the NotificationReceivedListener and the BACKGROUND_NOTIFICATION_TASK !!!

app is in background Android: working

app closed Android: not working (I think that’s normal because this test project doesn’t have the hacks mentioned here https://github.com/expo/expo/issues/14078#issuecomment-1041294084)

Hi! @brentvatne can you please assign someone to this bug? It’s here since 08/2021 and now it seems the workarounds don’t work either.

Thanks a lot!

Same issue here. addNotificationResponseReceivedListener is not working on Android if it is closed.

Been following and tried various workarounds of which none worked for me.

"expo": "~50.0.14", "expo-notifications": "~0.27.6",

Here’s my results from running the dev-client on a physical devices:

app in foreground Android: working iOS: working

app is in background Android: not working iOS: working

app closed Android: not working iOS: working

code:

import FontAwesome from '@expo/vector-icons/FontAwesome';
import {
  DarkTheme,
  DefaultTheme,
  ThemeProvider
} from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack, router } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import Constants from 'expo-constants';

import { useColorScheme } from '@/components/useColorScheme';
import { AuthProvider } from '@/context/Authcontext';

import { useState, useEffect, useRef } from 'react';
import { Platform } from 'react-native';
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
// eslint-disable-next-line import/named
import { NotificationResponse, Subscription } from 'expo-notifications';
import { storeToken } from '@/storage/pushStorage';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false
  })
});

SplashScreen.preventAutoHideAsync();

export { ErrorBoundary } from 'expo-router';

// eslint-disable-next-line @typescript-eslint/naming-convention
export const unstable_settings = {
  initialRouteName: '(tabs)'
};

function RootLayoutNav() {
  const colorScheme = useColorScheme();
  const notificationListener = useRef<Subscription>();
  const responseListener = useRef<Subscription>();

  const [storageInitialized, setStorageInitialized] = useState(false);

  async function registerForPushNotificationsAsync() {
    if (Platform.OS === 'android') {
      await Notifications.setNotificationChannelAsync('default', {
        name: 'default',
        importance: Notifications.AndroidImportance.MAX,
        vibrationPattern: [0, 250, 250, 250],
        lightColor: '#FF231F7C'
      });
    }

    if (!Device.isDevice) {
      console.log('Must use physical device for Push Notifications');

      return setStorageInitialized(true);
    }

    const { status: existingStatus } =
      await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;

    if (existingStatus !== 'granted') {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }

    if (finalStatus !== 'granted') {
      console.log('Failed to get push token for push notification!');

      return;
    }

    const projectId = Constants?.expoConfig?.extra?.eas?.projectId;
    const token = (
      await Notifications.getExpoPushTokenAsync({
        projectId: projectId ?? ''
      })
    ).data;
    await storeToken(token);
    setStorageInitialized(true);

    return token;
  }

  function handleNotificationResponse(response: NotificationResponse) {
    const url = response.notification.request.content.data?.url;
    console.log('URL:');
    console.log(url);

    if (url) {
      router.push(url);
    }
  }

  useEffect(() => {
    registerForPushNotificationsAsync().then((token) => {
      if (token) {
        notificationListener.current =
          Notifications.addNotificationReceivedListener((n) => {
            console.log('Notification received:', n);
          });

        responseListener.current =
          Notifications.addNotificationResponseReceivedListener(
            handleNotificationResponse
          );
      }
    });

    return () => {
      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
      notificationListener.current &&
        Notifications.removeNotificationSubscription(
          notificationListener.current
        );
      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
      responseListener.current &&
        Notifications.removeNotificationSubscription(responseListener.current);
    };
  }, []);

  if (!storageInitialized) {
    return null;
  }

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <AuthProvider>
        <Stack screenOptions={{ headerShown: false }}>
          <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        </Stack>
      </AuthProvider>
    </ThemeProvider>
  );
}

export default function RootLayout() {
  const [loaded, error] = useFonts({
    ...FontAwesome.font
  });

  useEffect(() => {
    if (error) throw error;
  }, [error]);

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return <RootLayoutNav />;
}

I tried digging into how expo-notifications is supposed to get push notification data when the app is opened via a push notification from a background/killed state and I couldn’t make sense of any of it, but that’s probably just my inexpertise. That said, it this package is definitely not listening for the intent data inside onCreate and onNewIntent in the MainActivity, which is where the notification data is always posted. So I wrote up a simple little native module which I use to get these initial notifications on Android. It just gives your app code access to the data associated with the triggering notification, and doesn’t conform to any of the expo-notifications interfaces.

Here the files are for those it may help. I just created a native library inside my folder with the help of create-react-native-library and then modified these files inside of it:

// ./my-app-notification-service.ts

// Note: This is my application code
import { popLastBackgroundNotificationData } from "react-native-notification-tap-data";

function initializeNotificationService() {
  AppState.addEventListener("change", async a => {
    if (a === "active") {
      popLastBackgroundNotificationData().then(notification => {
        if (notification) {
          //handle notification data
        }
      });

      const token = (await ExpoNotifications.getDevicePushTokenAsync()).data;
      handleNewEvent({ type: "push-token", token, reason: "app-foregrounded" });
    }
  });

  popLastBackgroundNotificationData().then(notification => {
    if (notification) {
          //handle notification data
    }
  });
}
// ./native-modules/react-native-notification-tap-data/src/index.tsx
import { NativeModules, Platform } from "react-native";

const LINKING_ERROR =
  `The package 'react-native-notification-tap-data' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ ios: "- You have run 'pod install'\n", default: "" }) +
  "- You rebuilt the app after installing the package\n" +
  "- You are not using Expo Go\n";

const NotificationTapData = NativeModules.NotificationTapData
  ? NativeModules.NotificationTapData
  : new Proxy(
      {},
      {
        get() {
          throw new Error(LINKING_ERROR);
        }
      }
    );

export function popLastBackgroundNotificationData(): Promise<object | null> {
  if (Platform.OS === "ios") {
    return Promise.resolve(null);
  }

  return NotificationTapData.popLastBackgroundNotificationData().then((a: string | null) => {
    return typeof a === "string" ? JSON.parse(a) : null;
  });
}
// ./native-modules/react-native-notification-tap-data/android/src/main/java/com/notificationtapdata/NotificationTapDataModule.kt

package com.notificationtapdata

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import android.os.Bundle
import android.util.Log
import org.json.JSONObject
import org.json.JSONArray

class NotificationTapDataModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

    override fun getName(): String {
        return NAME
    }

    @ReactMethod
    fun popLastBackgroundNotificationData(promise: Promise) {
        val lastNotification = sharedData
        sharedData = null;
        if (lastNotification != null) {
            promise.resolve(lastNotification.toString())
        } else {
            promise.resolve(null)
        }
      }

    companion object {
        const val NAME = "NotificationTapData"
        var sharedData: JSONObject? = null

        fun setLastBackgroundNotificationData(bundle: Bundle) {
            sharedData = bundleToJsonObject(bundle)
        }

        // Moved bundleToJsonObject here to make it accessible in a static context
        private fun bundleToJsonObject(bundle: Bundle): JSONObject {
            val jsonObject = JSONObject()
            val keys = bundle.keySet()
            for (key in keys) {
                when (val value = bundle.get(key)) {
                    is Bundle -> {
                        jsonObject.put(key, bundleToJsonObject(value))
                    }
                    is List<*> -> {
                        val jsonArray = org.json.JSONArray(value)
                        jsonObject.put(key, jsonArray)
                    }
                    else -> {
                        jsonObject.put(key, value)
                    }
                }
            }
            return jsonObject
        }
    }
}

The modified MainActivity with hooks to register the push notification:

//Omitted imports...

class MainActivity : ReactActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    setTheme(R.style.AppTheme);
    super.onCreate(null)
    //SAVE THE INITIAL NOTIFICATION DATA
    intent?.extras?.let {
      NotificationTapDataModule.setLastBackgroundNotificationData(it) //something like this
    }
  }

  override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    //SAVE THE BACKGROUNDED NOTIFICATION DATA
    intent?.extras?.let {
      NotificationTapDataModule.setLastBackgroundNotificationData(it) //something like this
    }
  }

  //Additional methods and properties...
}

And then I use the below Expo plugin to tweak my MainActivity.kt to inject the above registration snippets:

// ./native-modules/react-native-notification-tap-data/app.plugin.js

// This file modifies main activity so that the intent data is registered with the native module inside MainActivity.
// Only implemented in Kotlin, sorry!

const { withMainActivity } = require("@expo/config-plugins");

// Example config plugin to append a comment to MainActivity.java
const withCustomMainActivity = config => {
  return withMainActivity(config, async mainActivity => {
    let { contents, language } = mainActivity.modResults;

    if (language === "kt") {
      if (!contents.match(/override fun onNewIntent/)) {
        const inner = `
  override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
  }`;
        contents = contents.replace(/(override fun getMainComponentName\(\): String .+$)/m, `$1\n${inner}`);
      }

      if (!contents.includes("import android.content.Intent")) {
        contents = contents.replace(/(\n^class MainActivity)/m, `import android.content.Intent\n$1`);
      }

      if (!contents.includes("import com.notificationtapdata.NotificationTapDataModule")) {
        contents = contents.replace(/(\n^class MainActivity)/m, `import com.notificationtapdata.NotificationTapDataModule\n$1`);
      }

      if (!contents.includes("NotificationTapDataModule.setLastBackgroundNotificationData")) {
        const inner = `
    //Save intent data from tapped notification if it's available
    intent?.extras?.let {
      NotificationTapDataModule.setLastBackgroundNotificationData(it)
    }`;
        contents = contents.replace(/(override fun onCreate\(.*?\)[\s\S]+?super\.onCreate\(.*?\))/, `$1${inner}`);
        contents = contents.replace(/(override fun onNewIntent\(.*?\)[\s\S]+?super\.onNewIntent\(.*?\))/, `$1${inner}`);
      }
    } else if (language === "java") {
      throw new Error("Have yet implemented java MainActivity replacement!");
    }

    mainActivity.modResults.contents = contents;

    return mainActivity;
  });
};

module.exports = withCustomMainActivity;

And remember to add the new native package as a link in your package.json. E.g. something like this:

"react-native-notification-tap-data": "link:./native-modules/react-native-notification-tap-data/",

And remember to add it as a plugin in your app.config.js:

plugins: [ "react-native-notification-tap-data" ]

Hope this helps some people!

@christopherwalter Is there any reason you don’t want to do a solution that involves making a modification to the MainActivity? I understand the Expo team not wanting to require changes to the MainActivity to use expo-notifications on Android, but your library and literally every other library I’ve tried (I’ve tried all of them literally) seem to struggle a ton getting this initial notification data. I know plugins modifying MainActivity via regexes, etc are scary and brittle, but if the alternative is crucial functionality being broken on Android, I think people would prefer it.

For any one still stuck on this i make it working by calling the useLastNotificationResponse() hook just on application start App.tsx. In case you want to navigate your user to a specific page but you use a store or any thing that should should await data to set the proper navigator then here is my solution:

Export a const that contains the reference of your navigator export const navigationRef = React.createRef();

Then use it in your NavigationContainer ref attribute

<NavigationContainer
            ref={navigationRef}
            linking={LinkingConfiguration}
            theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
            {
                isLoadingComplete && <>
                    {(token !== null && connectedUser?.id) ?
                        <RootNavigator/> : <AuthNavigator/>
                    }
                </>
            }
        </NavigationContainer>

After that in your App.tsx or App.js witch is your main app add this code:

const lastNotif = useLastNotificationResponse();

const delay = ms => new Promise(res => setTimeout(res, ms));

useEffect(() => {
    const notif = lastNotif?.notification?.request?.content?.data?.notif
    if (!navigationRef.current && lastNotif) {
        (async () => {
            await checkNavigation() // loop until navigator is ready
            // navigate your user
        })()
    }
}, [lastNotif]);

const checkNavigation = async () => {
    if (!navigationRef.current) {
        await delay(500) // this is for call time exceeded error 
        await checkNavigation();
    }
    if (navigationRef.current)
        await delay(2000) // this is for giving my app time to set up my navigator
}

Hope it helps

NotificationResponseReceivedListener I can confirm that this issue still persists in Expo 44 for Android (Yet to confirm on iOS), the wired part is NotificationResponseReceivedListener is working perfectly fine in Custom Dev Client Build, regardless of the delay or the cold start, but same code does not world on the standalone build, before Expo 44 I had some hack around the same issue, but now that too no longer work.

Note: Foreground notifications with Data payload, working perfectly fine, but cold start notifications taps are not, even if NotificationResponseReceivedListener is in the root component.

Update 2: As suggested by cruzach this seems to work for Cold-start/killed taps on Android

const lastNotificationResponse = Notifications.useLastNotificationResponse();

  React.useEffect(() => {
    if (lastNotificationResponse) {
      alert("response");
    }
  }, [lastNotificationResponse]);

My implementation

 useEffect(() => {
    if (lastNotificationResponse) {
      if (Platform.OS === 'android') {
       handlesomeNavigation(lastNotificationResponse);
      }
    } else {
     dosomethingelse()
    }
  }, [lastNotificationResponse]);

lastNotificationResponse contains your notification data

not working on android

Exactly the same issue on my side on Expo 43, managed workflow.

Things I tried so far :

  • The following fixes the issue on iOS. Unfortunately not on Android:
const lastNotificationResponse = Notifications.useLastNotificationResponse();

 React.useEffect(() => {
   if (lastNotificationResponse) {
     alert("response");
   }
 }, [lastNotificationResponse]);
  • Moving the addNotificationResponseReceivedListener “as soon as possible” and outside of any component does not solve the issue as well.

@cruzach Thanks a lot for looking into this problem. I think it is a hard problem to debug since the feature is working with the debugger attached. The workaround, however, is working perfectly! Thanks.

app in foreground Android: working

Triggers the NotificationReceivedListener and the BACKGROUND_NOTIFICATION_TASK, maybe my fault, don't know, but it works

app is in background Android: working

Triggers BACKGROUND_NOTIFICATION_TASK

app closed Android: working

With the hacks mentioned here https://github.com/expo/expo/issues/14078#issuecomment-1041294084

In my expo.dev project I have added FCM legacy and V1 keys, If I force legacy, it works as mentioned above, but forcing V1, it fails no closed nor backgroud notifications.

Another strange behavior is that notification icon is ignored (on anoter project of mine) and when receiving a notification with FCM V1 there’s no popup, just a notification icon in the status bar

@mgscreativa Thank you SO much! I can confirm that FCM V1 causes the issue with addNotificationResponseReceivedListener and NONE of the workarounds work (I tried every single one).

However, when you switch back to FCM Legacy, this workaround (https://github.com/expo/expo/issues/14078#issuecomment-1041294084) works well in both SDK 49 and SDK 50.

I have not tried other workarounds such as hooks and methods expo provided for this issue (getLastNotificationResponseAsync and useLastNotificationResponse).

@douglowder This deserves immediate attention as FCM Legacy has less than 2 months before it gets cut off by Google (I believe).

@mgscreativa thanks for that update!

I can confirm toggling between legacy and FCM V1 does change notification behavior. Going back to legacy has fixed the confusing issues I was seeing with our production SDK 49 app (that includes a workaround “fix”). This seems very odd as I swear I tested this right after moving to FCM V1, so perhaps(?) it is some recent change. For me it acts differently in other ways also under FCM V1 now – our expo-notifications icon isn’t used and banners don’t appear under Android. Moving back to legacy both of those also act as expected. Very odd and a little sad that there are now two issues to contend with.

Hopefully it gets addressed before the legacy API cutoff date hits. 😬

HI guys, @douglowder and @lukagabadze. Have new findings!

In my case, If I enable FCM V1 service account key in my expo.dev project page, notifications start to fail, but if I left the good old FCM (Legacy) server key and delete the new FCM V1 key workarounds start to work again even in SDK 50!

@mgscreativa thanks for the info!

Also, the changelog entry you referenced above is actually a fix for an issue specific to some changes in the main branch needed for React Native 0.74 . Unfortunately I don’t think it will help us here 😄

Thanks @mgscreativa your “hack” looks good. I am actively working this issue, and also some related issues on Android around the useLastNotificationResponse() API. My understanding from your above comments is that this was working in SDK 49 and broke in SDK 50 – if that is not the case, please let me know.

@mgscreativa Can you post your package.json file please? And which workaround worked for you.

Basically this hack and read the blog post there https://github.com/expo/expo/issues/14078#issuecomment-1041294084

Hi! Just want to mention that my very same code works in SDK49, I only upgraded to SDK 50 and the Android notifications workaround stopped working

Just in case, sending my packages in SDK 49 (android notifications works with the workarounds) and the list of SDK 50 (which breaks the workarounds in android)

SDK 49 deps

"dependencies": {
    "@expo/config-plugins": "~7.2.2",
    "@expo/vector-icons": "^13.0.0",
    "@react-native-async-storage/async-storage": "1.18.2",
    "@react-native-community/netinfo": "9.3.10",
    "@react-navigation/bottom-tabs": "^6.5.8",
    "@react-navigation/native": "^6.0.2",
    "@react-navigation/native-stack": "^6.9.13",
    "expo": "~49.0.5",
    "expo-asset": "~8.10.1",
    "expo-constants": "~14.4.2",
    "expo-dev-client": "~2.4.13",
    "expo-device": "~5.4.0",
    "expo-font": "~11.4.0",
    "expo-linking": "~5.0.2",
    "expo-notifications": "~0.20.1",
    "expo-splash-screen": "~0.20.5",
    "expo-status-bar": "~1.6.0",
    "expo-system-ui": "~2.4.0",
    "expo-task-manager": "~11.3.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.72.10",
    "react-native-code-push": "^8.0.2",
    "react-native-code-push-plugin": "^1.0.3",
    "react-native-safe-area-context": "4.6.3",
    "react-native-screens": "~3.22.0",
    "react-native-url-polyfill": "^1.3.0",
    "react-native-web": "~0.19.6",
    "react-native-webview": "13.2.2",
    "zustand": "^4.4.7"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/react": "~18.2.14",
    "@typescript-eslint/eslint-plugin": "^5.61.0",
    "@typescript-eslint/parser": "^5.61.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-react": "^7.32.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "jest": "^29.2.1",
    "jest-expo": "~49.0.0",
    "prettier": "^3.0.1",
    "react-test-renderer": "18.2.0",
    "typescript": "^5.1.3"
  },
  "overrides": {
    "react-refresh": "~0.14.0"
  },
  "resolutions": {
    "react-refresh": "~0.14.0"
  },

SDK 50 deps

"dependencies": {
    "@expo/config-plugins": "^7.8.0",
    "@expo/vector-icons": "^14.0.0",
    "@react-native-async-storage/async-storage": "1.21.0",
    "@react-native-community/netinfo": "11.1.0",
    "@react-navigation/bottom-tabs": "^6.5.20",
    "@react-navigation/native": "^6.0.2",
    "@react-navigation/native-stack": "^6.9.26",
    "expo": "~50.0.17",
    "expo-asset": "~9.0.2",
    "expo-constants": "~15.4.6",
    "expo-dev-client": "~3.3.11",
    "expo-device": "~5.9.4",
    "expo-font": "~11.10.3",
    "expo-linking": "~6.2.2",
    "expo-notifications": "~0.27.7",
    "expo-splash-screen": "~0.26.4",
    "expo-status-bar": "~1.11.1",
    "expo-system-ui": "~2.9.4",
    "expo-task-manager": "~11.7.3",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.73.6",
    "react-native-code-push": "^8.2.2",
    "react-native-code-push-plugin": "^1.0.7",
    "react-native-safe-area-context": "4.8.2",
    "react-native-screens": "~3.29.0",
    "react-native-url-polyfill": "^2.0.0",
    "react-native-web": "~0.19.6",
    "react-native-webview": "13.6.4",
    "zustand": "^4.5.2"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/react": "~18.2.45",
    "@typescript-eslint/eslint-plugin": "^7.7.0",
    "@typescript-eslint/parser": "^7.7.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-react-hooks": "^4.6.0",
    "jest": "^29.2.1",
    "jest-expo": "~50.0.4",
    "prettier": "^3.2.5",
    "react-test-renderer": "18.2.0",
    "typescript": "^5.1.3"
  },

Hi! Just want to mention that my very same code works in SDK49, I only upgraded to SDK 50 and the Android notifications workaround stopped working

Having similar issues and none of the latest solutions in this thread are working for me, unfortunately.

Summary:

On Android, opening an incoming push notification does not trigger addNotificationResponseReceivedListener when the app is closed/backgrounded. It does work if the app is in the foreground. And works in all cases on iOS.

Dependencies:

"react-native": "0.73.6",
"expo": "~50.0.17",
"expo-notifications": "~0.27.7",
"expo-device": "~5.9.4",

Details:

I am using the exact config from the docs. And testing on EAS Preview and Development builds. Using a T607DL device running Android v13. Sending notifications from expo’s tool. The project is configured to use FCM API (V1).

Opening a push notification from a closed state does open the app, but this peice just does not fire:

responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
  console.log(response);
});

I have tried everything in this thread, including @djwood’s latest rec. The useLastNotification hook gives the same behavior, as well. Not sure how else to troubleshoot at this stage.

Does this work for anyone out of the box using the example from the docs?

For me neither addNotificationResponseReceivedListener nor useLastNotificationResponse work on android if the app is in background or killed. They are only called if the app is in the foreground. We are using latest expo and expo-notifications. This happens when running the app with expo run:android. I was not able to test a production build yet.

FYI, I’ve got it working with expo run:android as well as EAS preview build. I had to delete the ios & android folders and rebuild before getting it to work locally in all app modes. I’ll also add that both my Xcode and Android Studio are newly installed…

The issue ONLY occurs when the notifications are sent out through FCM. When scheduled locally, the NotificationResponseReceivedListener triggers like it should. The server code I use to trigger the push notifications is simply this:

firebaseAdmin.messaging().sendEach(info.map((a) => {
      return {
        notification: { body: a.data.body, title: a.data.title },
        data: a.data as any,
        android: {
          data: a.data as any,
          priority: "high",
          notification: {
            icon: "notification_icon",
            channelId: NotificationChannels.ollieNormal
          }
        },
        token: a.token
      };
    }))

@christopherwalter This provide any insight to you in being able to hunt down the underlying issue?

Currently my bigger concern is the upcoming 6/20/24 EOL for legacy Firebase APIs which AFAIKT Expo seems to be using under the hood for Android push. I’ve tried to confirm they are aware, even messaging their more active Discord staff several times, but never gotten a reply. So perhaps the writing is on the wall re: push.

@Codelica I can’t speak to the rest of your comment (though I hear you re: the tumultuous history of our approach toward expo-notifications) but I can address the FCM V1 concerns. We’ve been actively working on the changes needed to transition Android notifications from the FCM Legacy API to the FCM V1 API, and we’ll be communicating about it to everyone in the Expo community in the coming weeks. I’m sorry that your concerns seemed to fall on deaf ears, we’ll have to do better there.

We’re currently using Expo 49 and have it working via a similar hack as others have shown (set listener very early, capture any event, use further up the line). But I definitely share the concern here. This is a known bug that has existed for years now – but really IMO notifications have been left to die on the vine for a long time now.

At one point work was done to add image/media support, then it was rolled back before release never to appear again (despite it being a top voted feature). At one point the docs mentioned a potential forthcoming premium service (which we’d be first in line for) but that seems to have been scrubbed(?). Issues regarding push message performance now generally point people to services like OneSignal, etc.

Currently my bigger concern is the upcoming 6/20/24 EOL for legacy Firebase APIs which AFAIKT Expo seems to be using under the hood for Android push. I’ve tried to confirm they are aware, even messaging their more active Discord staff several times, but never gotten a reply. So perhaps the writing is on the wall re: push.

Still experiencing this and absolutely none of the workarounds mentioned work. So tldr is that right now with this library its impossible to get the initial notification on Android when the app is opened by touching a push notification in the killed state.

still an issue on Android even with initializing addNotificationResponseReceivedListener before any other react code

I believe it’s not the case. I struggled with that for a long time but looks like I found a solution that work in my case. To solve my problem I use getLastNotificationResponseAsync() not in main file (App.tsx) but in one of its child so after loading few libs and initialization some of the code. To be clear,

I use it in getInitialURL() function (of course only for android, iOS works OK for me.

const linking = {
  prefixes: [...],
  async getInitialURL() {
    if (Platform.OS === 'android') {
      const lastNotificationResponse = Notifications.getLastNotificationResponseAsync();
      lastNotificationResponse.then(response => {
        if (!!response) {
          storeNotificationFromStartup(response.notification);
        }
      })
    }
  }
}
<NavigationContainer ref={navigationRef}
                     linking={linking}>
  [...]
</NavigationContainer>

in storeNotificationFromStartup I save it into AsyncStorage and read that after API calls and initialization my app with data from backend.

Am also here in mid-2023 with this issue. I’m not seeing any response on either IOS or Android when the app is killed and a push notification is pressed.

Would be great if this limitation were added to the docs as it’s a considerable time waster!

Hi everyone, I’m also facing the issue on iOS. I’m using “addNotificationResponseReceivedListener” and I do an action depending the data I’m getting in the response, it’s working perfectly fine in foreground or background but whenever the app is killed and I’m getting a notification, when I tap the notification it does open my app and instantly crash.

Anyone having an idea please? I dont find any working workaround…

Hi @Codelica ! As stated in Edit 2 of your mentioned comment, I can confirm that this hack posted by @jackkinsella solves the issue https://github.com/expo/expo/issues/14078#issuecomment-1041294084

It’s not the state of the art of structured object oriented programming syntax, but works just fine.

@nakedgun thanks for the reply. I’ll definitely try exactly as you recommend, since you have it working as of 46 👍 But just a couple random comments (to clear my mind) that hopefully don’t add to the confusion.

Using Expo Go with my current implementation (Expo 46 + useLastNotificationResponse() hook) does work fine when the Go app is killed on Android, but does not work when the Go app is killed on iOS. Which is part of the reason this is such a head-scratcher on Android for me. I forget wether it was like this with the addNotificationResponseReceivedListener() emitter which I had originally though, sounds doubtful.

Also I still find it odd that the getLastNotificationResponseAsync() call wouldn’t have the “last” notification as well.

But at this point I’m up for anything, so I’ll refactor with things waaaaaay up the chain. 😎

Edit: Just to follow-up, I did get it working creating a temporary single-event listener right at the start of my code, outside of the React component tree, as suggested. Not ideal, but working. Considering the age and effect of this bug, I hope it gets some official attention. Maybe once EAS Notify is a thing?

On Expo 43, I still need to make a ‘expo-notifications+0.13.3.patch’ by myself to fix the issue on Android. iOS is working fine either with or without the patch

@Aaron46 can you please explain to me how to apply a patch in a standalone ?? managed workflow

I would also like to try the fix in a managed workflow. 🙏

This is how you apply the patch :

  • install a module called patch-package
  • create a new folder inside your project root name it “patches”
  • download the file in the link bellow and put it in “patches”
  • be sure that expo-notifications version in packages.json matches the version of the downloaded file .
  • build your project using eas build

https://gist.github.com/cruzach/b4853c615f1bcd4b736adbf3a789b577/archive/bac2c6ff7275201b434621a8227aeeacf018d1c5.zip

@Aaron46 can you please explain to me how to apply a patch in a standalone ?? managed workflow

@voku I could fixed on iOS upgrading to latest version of expo-updates:0.10.13. I didn’t test on Android.

That’s iOS, not Android, but thanks for sharing it here @RodolfoGS