expo: [ios][notifications] On iOS only, notifications to killed app are not delivered

Summary

Notifications to a killed app on iOS are not delivered. They are delivered to a running app on iOS. On Android, the same code build receives notifications in both a killed or running app.

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

bare

What platform(s) does this occur on?

iOS

SDK Version (managed workflow only)

No response

Environment

% expo diagnostics

  Expo CLI 4.3.2 environment info:
    System:
      OS: macOS 10.15.7
      Shell: 5.7.1 - /bin/zsh
    Binaries:
      Node: 14.15.1 - /usr/local/bin/node
      Yarn: 1.22.10 - /usr/local/bin/yarn
      npm: 6.14.8 - /usr/local/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    Managers:
      CocoaPods: 1.10.1 - /Users/me/.rbenv/shims/pod
    SDKs:
      iOS SDK:
        Platforms: iOS 14.4, DriverKit 20.2, macOS 11.1, tvOS 14.3, watchOS 7.2
    IDEs:
      Xcode: 12.4/12D4e - /usr/bin/xcodebuild
    npmPackages:
      expo: ^40.0.1 => 40.0.1 
      react: 16.13.1 => 16.13.1 
      react-dom: 16.13.1 => 16.13.1 
      react-native: ~0.63.4 => 0.63.4 
      react-native-web: ~0.13.12 => 0.13.18 
    npmGlobalPackages:
      expo-cli: 4.3.2
    Expo Workflow: bare

Dependencies from package.json:

  "dependencies": {
    "@expo/react-native-action-sheet": "^3.8.0",
    "@expo/vector-icons": "^12.0.2",
    "@ptomasroos/react-native-multi-slider": "^2.2.2",
    "@react-native-community/art": "https://github.com/Brewskey/art",
    "@react-native-community/image-editor": "^2.3.0",
    "@react-native-community/masked-view": "0.1.10",
    "@react-native-community/netinfo": "5.9.7",
    "@react-native-community/picker": "1.6.6",
    "@react-native-community/slider": "3.0.3",
    "@react-native-community/viewpager": "4.2.0",
    "@react-navigation/bottom-tabs": "^5.11.2",
    "@react-navigation/drawer": "^5.11.4",
    "@react-navigation/native": "^5.8.10",
    "@react-navigation/stack": "^5.12.8",
    "axios": "^0.21.0",
    "emoji-mart-native": "^0.6.2-beta",
    "expo": "^40.0.1",
    "expo-app-loader-provider": "~8.0.0",
    "expo-app-loading": "^1.0.0",
    "expo-asset": "~8.2.1",
    "expo-av": "^8.7.0",
    "expo-camera": "~9.1.0",
    "expo-constants": "~9.3.3",
    "expo-device": "~2.4.0",
    "expo-file-system": "~9.3.0",
    "expo-image-manipulator": "~8.4.0",
    "expo-intent-launcher": "~8.4.0",
    "expo-linear-gradient": "~8.4.0",
    "expo-linking": "~2.0.0",
    "expo-localization": "~9.1.0",
    "expo-notifications": "~0.8.2",
    "expo-permissions": "~10.0.0",
    "expo-splash-screen": "~0.8.1",
    "expo-status-bar": "~1.0.3",
    "expo-video-player": "^1.6.0",
    "hoist-non-react-statics": "^3.3.2",
    "i18n-js": "^3.7.1",
    "moment": "^2.28.0",
    "react": "16.13.1",
    "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
    "react-native-amazing-cropper": "https://github.com/myplaceonline/react-native-amazing-cropper.git",
    "react-native-calendar-picker": "^7.0.3",
    "react-native-calendars": "^1.437.0",
    "react-native-elements": "^3.0.0-alpha.1",
    "react-native-emoji-board": "^1.2.1",
    "react-native-emoji-input": "https://github.com/myplaceonline/react-native-emoji-input#customize",
    "react-native-emoji-selector": "^0.1.9",
    "react-native-gesture-handler": "~1.8.0",
    "react-native-gifted-chat": "^0.16.3",
    "react-native-google-places-autocomplete": "^2.1.1",
    "react-native-image-rotate": "^2.1.0",
    "react-native-keyboard-spacer": "^0.4.1",
    "react-native-linear-gradient": "^2.5.6",
    "react-native-paper": "^4.4.1",
    "react-native-progress": "https://github.com/Brewskey/react-native-progress",
    "react-native-progress-circle": "^2.1.0",
    "react-native-progress-wheel": "^1.0.5",
    "react-native-radio-button": "^2.0.1",
    "react-native-ratings": "^7.3.0",
    "react-native-reanimated": "~1.13.0",
    "react-native-safe-area-context": "3.1.9",
    "react-native-screens": "~2.15.2",
    "react-native-semi-circle-progress": "https://github.com/pajicf/react-native-semi-circle-progress",
    "react-native-sideswipe": "^1.5.0",
    "react-native-simple-radio-button": "^2.7.4",
    "react-native-snap-carousel": "^3.9.1",
    "react-native-svg": "12.1.0",
    "react-native-swipe-list-view": "^3.2.5",
    "react-native-vector-icons": "^7.1.0",
    "react-native-webview": "11.0.0",
    "react-native-wizard": "^2.1.0",
    "react-number-format": "^4.4.1",
    "react-redux": "^7.2.2",
    "react-tween-state": "^0.1.5",
    "redux": "^4.0.5",
    "tslib": "^2.0.1"
  },

Reproducible demo or steps to reproduce from a blank project

Below is the relevant code which is at the top of App.js so that it runs as soon as possible. Neither the debug lines in Notifications.setNotificationHandler nor Notifications.addNotificationResponseReceivedListener are printed when a notification is clicked on a killed app.

import React from 'react';
import { Text, View } from 'react-native';
import Constants from 'expo-constants';

// Initialize core debug code
let accumulatedDebug = "";
let deviceInfo = null;

const MAX_DEBUG = 1000000;
const MAX_CONSOLE_LOG_SIZE = 9500;

function appendToLog(str) {
  accumulatedDebug = accumulatedDebug + "\n" + str;

  // Limit how much memory debug is storing
  if (accumulatedDebug.length > MAX_DEBUG) {
    accumulatedDebug = "[... truncated ...] " + accumulatedDebug.substring(MAX_DEBUG / 2);
  }
}

function getLogPrefix() {
  let result = "[" + new Date().toISOString() + "] ";
  if (deviceInfo != null && deviceInfo.installationId) {
    result += "{" + deviceInfo.installationId + "} ";
  }
  return result;
}

function info(message, skipConsole) {
  const str = getLogPrefix() + message;
  if (!skipConsole) {
    console.log(str);
  }
  appendToLog(str);
}

function debug(message) {
  const str = getLogPrefix() + message;
  if (str.length > MAX_CONSOLE_LOG_SIZE) {
    console.debug(str.substr(0, MAX_CONSOLE_LOG_SIZE) + " [...] TRUNCATED");
  } else {
    console.debug(str);
  }
  appendToLog(str);
}

function toJSONPretty(obj) {
  try {
    return JSON.stringify(obj, null, 2);
  } catch (e) {
    info("Error in toJSONPretty for " + (typeof obj) + " : " + e);
  }
}

info("Application started. Version: " + Constants.manifest.version);

// Register for notifications as early as possible

// https://docs.expo.io/versions/latest/sdk/notifications/
// https://docs.expo.io/push-notifications/sending-notifications/
import * as Notifications from 'expo-notifications';

const NOTIFICATION_RECEIVED_WHEN_APP_IN_FOREGROUND = "notificationWithAppInForeground";
const NOTIFICATION_CLICKED_AND_APP_IN_FOREGROUND_OR_BACKGROUND_OR_CLOSED = "notificationClicked";

function globalReceivedNotificationForeground(notification) {
  globalProcessNotification(notification, NOTIFICATION_RECEIVED_WHEN_APP_IN_FOREGROUND);
}

function globalClickedNotification(response) {
  globalProcessNotification(response.notification, NOTIFICATION_CLICKED_AND_APP_IN_FOREGROUND_OR_BACKGROUND_OR_CLOSED);
}

let isAppScreensWithDrawerComponentReady = false;
let pendingNotification = null;

function globalProcessNotification(notification, source) {

  // Most notifications are actually handled in AppScreensWithDrawerComponent
  info("globalProcessNotification from " + source + ", notification: " + toJSONPretty(notification));

  try {

    const data = notification.request.content.data;

    info("globalProcessNotification isAppScreensWithDrawerComponentReady: " + isAppScreensWithDrawerComponentReady);

    if (pendingNotification == null && !isAppScreensWithDrawerComponentReady && source == NOTIFICATION_CLICKED_AND_APP_IN_FOREGROUND_OR_BACKGROUND_OR_CLOSED) {
      info("globalProcessNotification queued pendingNotification");
      pendingNotification = notification;
    }

  } catch (e) {
    handleException(e, "globalProcessNotification");
  }

}

function registerGlobalNotificationHandler() {
  try {
    // https://docs.expo.io/push-notifications/receiving-notifications/

    // behavior for when notifications are received while your app is foregrounded
    Notifications.setNotificationHandler({
      handleNotification: (notification) => {
        info("Notifications: handleNotification notification: " + toJSONPretty(notification));
        return {
          shouldShowAlert: true,
          shouldPlaySound: false,
          shouldSetBadge: false,
        };
      },
      handleSuccess: (notificationId) => {
        info("Notifications: handleSuccess notificationId: " + notificationId);
      },
      handleError: (error) => {
        info("Notifications: handleError error: " + error);
      },
    });

    // There are two different subscriptions so that you can easily address cases where a
    // notification comes in while your app is open and foregrounded, and cases where a notification
    // comes in while your app is backgrounded or closed, and the user taps on the notification.

    // fired whenever a notification is received while the app is foregrounded
    Notifications.addNotificationReceivedListener(globalReceivedNotificationForeground);

    // fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed).
    Notifications.addNotificationResponseReceivedListener(globalClickedNotification);

  } catch (e) {
    info("Error in registerGlobalNotificationHandler: " + e);
  }
}

registerGlobalNotificationHandler();

export default function App() {
[...]

I’m a bit confused by all the issues related to this (#12228, #11933, #11343, #9866, #9478, #7787, and #6943). It seems like that since I’m using the bare workflow, I can just run expo install expo-notifications@0.11.0 to get the latest fix for this; is that right? And do I need to use useLastNotificationResponse (or getLastNotificationResponseAsync) or should Notifications.addNotificationResponseReceivedListener work?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 15 (8 by maintainers)

Most upvoted comments

I’m working with SDK 41 also, and my addNotificationResponseReceivedListener() is not called when the app is killed.

My code is :


useEffect(() => {
    async function start () {

      Notifications.addNotificationResponseReceivedListener(async (data) => {
          await Linking.openURL("www.google.fr");
      });

      await Font.loadAsync({
        Roboto: require('native-base/Fonts/Roboto.ttf'),
        Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
      });

      setFontLoading(false);
    }
    start()
  }, [])

in App.js

The listener is only work if app work in background.

@oron11 @kevgrig I’m working with Expo SDK 41 and expo-notifications ~0.11.6 and I’m still having the same issue: no matter where I place addNotificationResponseReceivedListener, if the app is killed this listener is not working. What expo-notifications version do you have? And where did you ended up placing the listener?