Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[🐛] Bug Report Title - Data only notifications on android don't arrive when app is closed (EXPO) #8443

Closed
2 of 10 tasks
OgDev-01 opened this issue Apr 4, 2025 · 16 comments
Closed
2 of 10 tasks
Labels
Needs Attention platform: android plugin: messaging FCM only - ( messaging() ) - do not use for Notifications type: bug New bug report

Comments

@OgDev-01
Copy link

OgDev-01 commented Apr 4, 2025

Issue

Describe your issue here
I'm using @react-native-firebase/messaging with @notifee/react-native to handle notifications on my app... But android notifications don't arrive on devices when the app is closed.

Key things to note

  • We are using data-only payload as described in https://rnfirebase.io/messaging/usage#data-only-messages.
  • If a trigger a notification when the app is closed, nothing shows up, but if i open the app to check, the notification pops up.
  • This behavior only happens on android, IOS works perfect and as expected.

I did some research and bumped into #1238 but the conversations there seems quite old and i'm using Expo managed workflow.

Message Payload

{
  "android": {
    "asForegroundService": false,
    "autoCancel": true,
    "badgeIconType": 2,
    "channelId": "reminders",
    "chronometerDirection": "up",
    "circularLargeIcon": false,
    "colorized": false,
    "defaults": [
      -1
    ],
    "groupAlertBehavior": 0,
    "groupSummary": false,
    "importance": 3,
    "largeIcon": "https://golivwell-s3-bucket.s3.eu-central-1.amazonaws.com/c1a8c098-3b2c-428a-ba02-8462cac2ca6b_products.jpg",
    "lightUpScreen": false,
    "localOnly": false,
    "loopSound": false,
    "ongoing": false,
    "onlyAlertOnce": false,
    "showChronometer": false,
    "showTimestamp": true,
    "smallIcon": "ic_launcher",
    "timestamp": 1743770458521,
    "visibility": 0
  },
  "body": "I did this again version 2",
  "data": {
    "notifee": {
      "body": "I did this again version 2",
      "data": "[Object]",
      "deepLink": "string",
      "imageUrl": "https://golivwell-s3-bucket.s3.eu-central-1.amazonaws.com/c1a8c098-3b2c-428a-ba02-8462cac2ca6b_products.jpg",
      "title": "Hello from swagger",
      "type": "MEDICATION_REMINDER"
    }
  },
  "id": "zWNs9jm0ZwW1IYXxWuvZ",
  "title": "Hello from swagger"
}

Project Files

Javascript

Click To Expand

package.json:

{
  "name": "Golivwell",
  "version": "1.0.4",
  "private": true,
  "main": "expo-router/entry",
  "scripts": {
    "start": "cross-env EXPO_NO_DOTENV=1 expo start",
    "prebuild": "cross-env EXPO_NO_DOTENV=1 pnpm expo prebuild",
    "android": "cross-env EXPO_NO_DOTENV=1 expo run:android",
    "ios": "cross-env EXPO_NO_DOTENV=1 expo run:ios",
    "doctor": "npx expo-doctor",
    "preinstall": "npx only-allow pnpm",
    "start:staging": "cross-env APP_ENV=staging pnpm run start",
    "prebuild:staging": "cross-env APP_ENV=staging pnpm run prebuild",
    "android:staging": "cross-env APP_ENV=staging pnpm run android",
    "ios:staging": "cross-env APP_ENV=staging pnpm run ios",
    "start:production": "cross-env APP_ENV=production pnpm run start",
    "prebuild:production": "cross-env APP_ENV=production pnpm run prebuild",
    "android:production": "cross-env APP_ENV=production pnpm run android",
    "ios:production": "cross-env APP_ENV=production pnpm run ios",
    "build:development:ios": "cross-env APP_ENV=development EXPO_NO_DOTENV=1 eas build --profile development --platform ios",
    "build:development:android": "cross-env APP_ENV=development EXPO_NO_DOTENV=1 eas build --profile development --platform android",
    "build:staging:ios": "cross-env APP_ENV=staging EXPO_NO_DOTENV=1 eas build --profile staging --platform ios",
    "build:staging:android": "cross-env APP_ENV=staging EXPO_NO_DOTENV=1 eas build --profile staging --platform android",
    "build:production:ios": "cross-env APP_ENV=production EXPO_NO_DOTENV=1 eas build --profile production --platform ios",
    "build:production:android": "cross-env APP_ENV=production EXPO_NO_DOTENV=1 eas build --profile production --platform android",
    "update:prod": "cross-env APP_ENV=production eas update --clear-cache --channel production --message \"Automated update to prod\"",
    "update:staging": "cross-env APP_ENV=staging eas update --clear-cache --channel staging --message \"Automated update to staging\"",
    "postinstall": "patch-package && husky install",
    "app-release": "cross-env SKIP_BRANCH_PROTECTION=true np --no-publish --no-cleanup --no-release-draft --branch beta",
    "version": "pnpm run prebuild && git add .",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
    "format": "prettier --write .",
    "type-check": "tsc  --noemit",
    "lint:translations": "eslint ./src/translations/ --fix --ext .json",
    "test": "jest",
    "test:ci": "pnpm run test --coverage",
    "test:watch": "pnpm run test --watch",
    "install-maestro": "curl -Ls 'https://get.maestro.mobile.dev' | bash",
    "e2e-test": "maestro test .maestro/ -e APP_ID=com.obytes.development"
  },
  "dependencies": {
    "@expo-google-fonts/poppins": "^0.2.3",
    "@expo/metro-runtime": "^3.2.3",
    "@gluestack-ui/accordion": "^1.0.6",
    "@gluestack-ui/nativewind-utils": "^1.0.23",
    "@gluestack-ui/overlay": "^0.1.15",
    "@gluestack-ui/popover": "^0.1.37",
    "@gluestack-ui/toast": "^1.0.7",
    "@gorhom/bottom-sheet": "^4.6.3",
    "@hookform/resolvers": "^2.9.11",
    "@legendapp/motion": "^2.3.0",
    "@notifee/react-native": "^9.1.8",
    "@react-native-community/datetimepicker": "8.0.1",
    "@react-native-firebase/app": "^21.12.0",
    "@react-native-firebase/messaging": "^21.12.0",
    "@react-navigation/bottom-tabs": "^6.5.20",
    "@react-navigation/material-top-tabs": "^6.6.2",
    "@react-navigation/native": "^6.1.17",
    "@react-navigation/native-stack": "^6.9.26",
    "@shopify/flash-list": "1.6.4",
    "@tanstack/react-query": "^5.37.1",
    "app-icon-badge": "^0.0.15",
    "axios": "^1.7.1",
    "expo": "~51.0.39",
    "expo-build-properties": "~0.12.5",
    "expo-constants": "~16.0.2",
    "expo-dev-client": "~4.0.29",
    "expo-device": "^6.0.2",
    "expo-file-system": "~17.0.1",
    "expo-font": "~12.0.10",
    "expo-image": "~1.13.0",
    "expo-image-picker": "^15.1.0",
    "expo-linear-gradient": "~13.0.2",
    "expo-linking": "~6.3.1",
    "expo-localization": "~15.0.3",
    "expo-notifications": "~0.28.19",
    "expo-router": "~3.5.24",
    "expo-sharing": "~12.0.1",
    "expo-splash-screen": "0.27.7",
    "expo-status-bar": "~1.12.1",
    "expo-system-ui": "~3.0.7",
    "expo-updates": "~0.25.28",
    "expo-web-browser": "~13.0.3",
    "gifted-charts-core": "^0.1.30",
    "i18next": "^22.5.1",
    "lodash.memoize": "^4.1.2",
    "moti": "^0.28.1",
    "nativewind": "^4.0.36",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-error-boundary": "^3.1.4",
    "react-hook-form": "^7.51.4",
    "react-i18next": "^12.3.1",
    "react-native-avoid-softinput": "^4.0.2",
    "react-native-calendars": "^1.1306.0",
    "react-native-chart-kit": "^6.12.0",
    "react-native-css-interop": "0.0.36",
    "react-native-flash-message": "^0.4.2",
    "react-native-gesture-handler": "~2.16.2",
    "react-native-gifted-charts": "^1.4.29",
    "react-native-image-picker": "^7.1.2",
    "react-native-keyboard-controller": "^1.14.4",
    "react-native-marked": "^6.0.5",
    "react-native-mmkv": "2.6.3",
    "react-native-otp-entry": "^1.7.0",
    "react-native-pager-view": "6.3.0",
    "react-native-permissions": "^4.1.5",
    "react-native-qrcode-svg": "^6.3.2",
    "react-native-reanimated": "~3.10.1",
    "react-native-restart": "0.0.27",
    "react-native-safe-area-context": "4.10.5",
    "react-native-screens": "~3.31.1",
    "react-native-svg": "15.2.0",
    "react-native-tab-view": "^4.0.1",
    "react-native-web": "~0.19.11",
    "react-native-webview": "13.8.6",
    "react-query-kit": "^3.3.0",
    "tailwind-variants": "^0.1.20",
    "terra-react": "^1.6.15",
    "zod": "^3.23.8",
    "zustand": "^4.5.2",
    "react-native": "0.74.5"
  },
  "devDependencies": {
    "@babel/core": "^7.24.5",
    "@commitlint/cli": "^17.8.1",
    "@commitlint/config-conventional": "^17.8.1",
    "@dev-plugins/react-navigation": "^0.0.6",
    "@dev-plugins/react-query": "^0.0.6",
    "@expo/config": "~9.0.2",
    "@react-native-community/eslint-config": "^3.2.0",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react-native": "^12.5.0",
    "@types/i18n-js": "^3.8.9",
    "@types/jest": "^29.5.12",
    "@types/lodash.memoize": "^4.1.9",
    "@types/react": "~18.2.79",
    "@types/react-test-renderer": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^5.62.0",
    "@typescript-eslint/parser": "^5.62.0",
    "babel-plugin-module-resolver": "^5.0.2",
    "clsx": "^2.1.1",
    "cross-env": "^7.0.3",
    "date-fns": "^3.6.0",
    "dotenv": "^16.4.5",
    "eslint": "^8.57.0",
    "eslint-plugin-i18n-json": "^4.0.0",
    "eslint-plugin-simple-import-sort": "^10.0.0",
    "eslint-plugin-tailwindcss": "^3.15.2",
    "eslint-plugin-testing-library": "^6.2.2",
    "eslint-plugin-unicorn": "^46.0.1",
    "eslint-plugin-unused-imports": "^2.0.0",
    "husky": "^8.0.3",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "jest-expo": "~51.0.4",
    "jest-junit": "^16.0.0",
    "lint-staged": "^13.3.0",
    "metro-babel-register": "^0.73.10",
    "np": "^7.7.0",
    "prettier": "^2.8.8",
    "react-test-renderer": "^18.3.1",
    "tailwind-merge": "^2.5.2",
    "tailwindcss": "3.3.2",
    "ts-jest": "^29.1.2",
    "typescript": "^5.3.3"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/integrated-wellness-inc/livwell-mobile-app.git"
  },
  "packageManager": "[email protected]",
  "osMetadata": {
    "initVersion": "6.1.0"
  }
}

firebase.json for react-native-firebase v6:

# N/A

iOS

Click To Expand

ios/Podfile:

  • I'm not using Pods
  • I'm using Pods and my Podfile looks like:
# N/A

AppDelegate.m:

# N/A


Android

Click To Expand

Have you converted to AndroidX?

  • my application is an AndroidX application?
  • I am using android/gradle.settings jetifier=true for Android compatibility?
  • I am using the NPM package jetifier for react-native compatibility?

android/build.gradle:

android/app/build.gradle:

apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"

def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()

static def versionToNumber(major, minor, patch) {
  return patch * 100 + minor * 10000 + major * 1000000
}

def getRNVersion() {
  def version = providers.exec {
    workingDir(projectDir)
    commandLine("node", "-e", "console.log(require('react-native/package.json').version);")
  }.standardOutput.asText.get().trim()

  def coreVersion = version.split("-")[0]
  def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() }

  return versionToNumber(
      major,
      minor,
      patch
  )
}
def rnVersion = getRNVersion()

/**
 * This is the configuration block to customize your React Native Android app.
 * By default you don't need to apply any configuration, just uncomment the lines you need.
 */
react {
    entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
    reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
    hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
    codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()

    // Use Expo CLI to bundle the app, this ensures the Metro config
    // works correctly with Expo projects.
    cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
    bundleCommand = "export:embed"

    /* Folders */
    //   The root of your project, i.e. where "package.json" lives. Default is '..'
    // root = file("../")
    //   The folder where the react-native NPM package is. Default is ../node_modules/react-native
    // reactNativeDir = file("../node_modules/react-native")
    //   The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
    // codegenDir = file("../node_modules/@react-native/codegen")

    /* Variants */
    //   The list of variants to that are debuggable. For those we're going to
    //   skip the bundling of the JS bundle and the assets. By default is just 'debug'.
    //   If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
    // debuggableVariants = ["liteDebug", "prodDebug"]

    /* Bundling */
    //   A list containing the node command and its flags. Default is just 'node'.
    // nodeExecutableAndArgs = ["node"]

    //
    //   The path to the CLI configuration file. Default is empty.
    // bundleConfig = file(../rn-cli.config.js)
    //
    //   The name of the generated asset file containing your JS bundle
    // bundleAssetName = "MyApplication.android.bundle"
    //
    //   The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
    // entryFile = file("../js/MyApplication.android.js")
    //
    //   A list of extra flags to pass to the 'bundle' commands.
    //   See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
    // extraPackagerArgs = []

    /* Hermes Commands */
    //   The hermes compiler command to run. By default it is 'hermesc'
    // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
    //
    //   The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
    // hermesFlags = ["-O", "-output-source-map"]

    if (rnVersion >= versionToNumber(0, 75, 0)) {
        /* Autolinking */
        autolinkLibrariesWithApp()
    }
}

/**
 * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
 */
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()

/**
 * The preferred build flavor of JavaScriptCore (JSC)
 *
 * For example, to use the international variant, you can use:
 * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
 *
 * The international variant includes ICU i18n library and necessary data
 * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
 * give correct results when using with locales other than en-US. Note that
 * this variant is about 6MiB larger per architecture than default.
 */
def jscFlavor = 'org.webkit:android-jsc:+'

android {
    ndkVersion rootProject.ext.ndkVersion

    buildToolsVersion rootProject.ext.buildToolsVersion
    compileSdk rootProject.ext.compileSdkVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0.4"
    }
    signingConfigs {
        debug {
            storeFile file('debug.keystore')
            storePassword 'android'
            keyAlias 'androiddebugkey'
            keyPassword 'android'
        }
    }
    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://reactnative.dev/docs/signed-apk-android.
            signingConfig signingConfigs.debug
            shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
            crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
        }
    }
    packagingOptions {
        jniLibs {
            useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
        }
    }
}

// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
    // Split option: 'foo,bar' -> ['foo', 'bar']
    def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
    // Trim all elements in place.
    for (i in 0..<options.size()) options[i] = options[i].trim();
    // `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
    options -= ""

    if (options.length > 0) {
        println "android.packagingOptions.$prop += $options ($options.length)"
        // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
        options.each {
            android.packagingOptions[prop] += it
        }
    }
}

dependencies {
    // The version of react-native is set by the React Native Gradle Plugin
    implementation("com.facebook.react:react-android")

    def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
    def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
    def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";

    if (isGifEnabled) {
        // For animated gif support
        implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
    }

    if (isWebpEnabled) {
        // For webp support
        implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
        if (isWebpAnimatedEnabled) {
            // Animated webp support
            implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
        }
    }

    if (hermesEnabled.toBoolean()) {
        implementation("com.facebook.react:hermes-android")
    } else {
        implementation jscFlavor
    }
}

if (rnVersion < versionToNumber(0, 75, 0)) {
    apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
    applyNativeModulesAppBuildGradle(project)
}

apply plugin: 'com.google.gms.google-services'

android/settings.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext {
        buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
        minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23')
        compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34')
        targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
        kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23'

        ndkVersion = "26.1.10909125"
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.gms:google-services:4.4.1'
        classpath('com.android.tools.build:gradle')
        classpath('com.facebook.react:react-native-gradle-plugin')
        classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
    }
}

apply plugin: "com.facebook.react.rootproject"

allprojects {
    repositories {
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
        }
        maven {
            // Android JSC is installed from npm
            url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist'))
        }

        google()
        mavenCentral()
        maven { url 'https://www.jitpack.io' }
    }
}

MainApplication.java:

// N/A

AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.RECORD_AUDIO"/>
  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  <uses-permission android:name="android.permission.VIBRATE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <queries>
    <intent>
      <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.BROWSABLE"/>
      <data android:scheme="https"/>
    </intent>
  </queries>
  <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
    <meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
    <meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>

    <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
      </intent-filter>
    </activity>
    <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
  </application>
</manifest>


Environment

Click To Expand

react-native info output:

 OUTPUT GOES HERE
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • react-native-firebase version you're using that has this issue:
    • e.g. 5.4.3
  • Firebase module(s) you're using that has the issue:
    • e.g. Instance ID
  • Are you using TypeScript?
    • Y/N & VERSION


@MichaelVerdon MichaelVerdon added platform: android plugin: messaging FCM only - ( messaging() ) - do not use for Notifications labels Apr 7, 2025
@MichaelVerdon
Copy link
Collaborator

Hey there, can you show me a usage example of how you are using the background handler?

@OgDev-01
Copy link
Author

OgDev-01 commented Apr 7, 2025

Hey there, can you show me a usage example of how you are using the background handler?

Here is my handler at the top of my root _layout file @MichaelVerdon

setBackgroundMessageHandler(
  messaging,
  async (message: FirebaseMessagingTypes.RemoteMessage) => {

    if (!message.notification) {
      try {
        await notifee.incrementBadgeCount();
        await handleBackgroundMessage(message);
      } catch (error) {
        console.error("Error displaying notification:", error);
      }
    }
    return Promise.resolve();
  }
);

As instructed in the docs, always return a resolved promise.

handleBackgroundMessage is a utility function that calls notifee.displayNotification to trigger the notifications locally with the remote data

@OgDev-01
Copy link
Author

OgDev-01 commented Apr 8, 2025

@MichaelVerdon , are there other informations you'll need on this?. It's a bug that needs to be fixed on my end 🙏

@OgDev-01 OgDev-01 changed the title [🐛] Bug Report Title - Data only notifications on android don't arrive when app is closed [🐛] Bug Report Title - Data only notifications on android don't arrive when app is closed (EXPO) Apr 8, 2025
@MichaelVerdon
Copy link
Collaborator

Hi there, i am currently investigating it. My testing app refused to work yesterday but it is all good today. 😄

@mikehardy
Copy link
Collaborator

@OgDev-01

To allow data-only messages to trigger the background handler, you must set the "priority" to "high" on Android

I reformatted the JSON you send in the FCM so it was readable and I could scan for that key on a hunch. I don't see it

@MichaelVerdon
Copy link
Collaborator

Hey there, I did receive a notification whilst the app was in a terminated state and it opened the app. It did not work until I put this in my AndroidManifest (registering the service): https://firebase.google.com/docs/cloud-messaging/android/client

<service
        android:name="io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService"
        android:exported="true"
        android:permission="com.google.android.c2dm.permission.SEND">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT"/>
        </intent-filter>
    </service>

And you need to use this method getInitialNotification:

messaging()
      .getInitialNotification()
      .then((remoteMessage) => {
        if (remoteMessage) {
          console.log('App opened from quit state with message:', remoteMessage.data);
          setMessage(remoteMessage.data?.message || 'App opened with background message');
        }
      });

https://rnfirebase.io/reference/messaging#getInitialNotification

I will warn you that it has been a bit flaky and I've observed messaging can be flaky with these things across other platforms making me think if it is an Android-Firebase native issue

@MichaelVerdon
Copy link
Collaborator

MichaelVerdon commented Apr 8, 2025

Also make sure there is a notification in your payload, I don't think data-only works. What purpose do you need it to be data only?

@mikehardy
Copy link
Collaborator

Also make sure there is a notification in your payload, I don't think data-only works. What purpose do you need it to be data only?

That's not true - data-only on android works fine. iOS is very problematic and at the whim of the OS as to whether it will deliver or not - it will sometimes, enough to trick you into thinking it's reliable when it isn't - but android data-only works great in my experience

@mikehardy
Copy link
Collaborator

<service
        android:name="io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService"
        android:exported="true"
        android:permission="com.google.android.c2dm.permission.SEND">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT"/>
        </intent-filter>
    </service>

You shouldn't need to do that, during AndroidManifest merge, that is added by the android gradle build process into the final combined AndroidManifest for the app, coming from this chunk here:

<receiver
android:name=".ReactNativeFirebaseMessagingReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</receiver>

To put that in the app-level AndroidManifest is to basically duplicate it - if you open an app with react-native-firebase messaging integrated and check the "Combined" AndroidManifest tab you should see it in there, merged in automatically

I've never had to manually put this in my apps - so I suspect a test methodology error somehow when it had a negative delivery test result without it

@MichaelVerdon
Copy link
Collaborator

Thanks for clarification @mikehardy I still think it might be worth trying anyways as I managed to get it to work on my end doing that but on the other hand, I don't think I needed to do that with FlutterFire 🤔

@OgDev-01
Copy link
Author

OgDev-01 commented Apr 8, 2025

@OgDev-01

To allow data-only messages to trigger the background handler, you must set the "priority" to "high" on Android

I reformatted the JSON you send in the FCM so it was readable and I could scan for that key on a hunch. I don't see it

Honestly, i think the formatted payload looks different from the initial.

This is the correct payload the app receives

{
    "originalPriority": 1,
    "priority": 1,
    "sentTime": 1744117869665,
    "data": {
        "notifee": "{\"title\":\"string\",\"body\":\"string\",\"data\":{\"additionalProp1\":\"string\",\"additionalProp2\":\"string\",\"additionalProp3\":\"string\"},\"deepLink\":\"string\",\"imageUrl\":\"string\",\"type\":\"MEDICATION_REMINDER\"}",
        "type": "MEDICATION_REMINDER",
        "body": "string",
        "title": "string",
        "additionalProp3": "string",
        "additionalProp1": "string",
        "additionalProp2": "string",
        "imageUrl": "string"
    },
    "from": "688826572618",
    "messageId": "0:1744117869689285%2bc11c71cad56011",
    "ttl": 259200,
    "collapseKey": "string"
}

Our api uses the firebase admin sdk in java, and we are setting the priority for every data message payload that is sent over.

@OgDev-01
Copy link
Author

OgDev-01 commented Apr 8, 2025

This is our snippet on the backend using the firebase admin

private AndroidConfig buildAndroidConfig(PushNotificationRequest request) {
        return AndroidConfig.builder()
                .setPriority(AndroidConfig.Priority.HIGH)
                .setCollapseKey(request.getTitle())
                .setDirectBootOk(true)
                .setTtl(259200000)
                .build();
    }

    private ApnsConfig buildApnsConfig(PushNotificationRequest request) {
        ApnsConfig.Builder builder = ApnsConfig.builder();
        builder.setAps(
                Aps.builder()
                        .setContentAvailable(true)
                        .build());

        if (request.getDeepLink() != null) {
            builder.putHeader("link", request.getDeepLink());
        }

        Map<String, String> apnsHeaders = new HashMap<>();
        apnsHeaders.put("apns-priority", "5");
        apnsHeaders.put("apns-push-type", "background");

        builder.putAllHeaders(apnsHeaders);
        return builder.build();
    }

this line setPriority(AndroidConfig.Priority.HIGH) and setContentAvailable(true) are from my understanding of the docs the required attributes to enable ios and android data-only message deliver. https://rnfirebase.io/messaging/usage#data-only-messages

I all looks like we are doing all the needful 😟 , but it just seem not to work on android. I appreciate the efforts and quick response from the team 👍 . Any idea what i'm doing wrong? @mikehardy

@mikehardy
Copy link
Collaborator

Any idea what i'm doing wrong

Nope sorry, out of ideas. You might try https://dontkillmyapp.com/, make sure you don't have power saving on, check adb logcat etc. We don't reproduce and the module definitely works for some, so you have a project-specific error that is not really actionable here - it's not going to result in a change in this repo

@OgDev-01
Copy link
Author

OgDev-01 commented Apr 8, 2025

Leaving this here for anyone who gets into same situation.

I was able to solve the problem by avoiding the problem all together 😄 . I initially opted for data-only messages due to some limitations on IOS > 17 #7548. But fixing it with the walk around introduced this issue on android.

What i ended up doing?

I still sticked to my data only message payload, but this time just for IOS. and i added notification key to the payload for android, and this solved my issue. Thanks @mikehardy and @MichaelVerdon for the support 👍

@mikehardy
Copy link
Collaborator

interesting - usually it is the other way around, data-only works well for android in my experience but on ios they are problematic

I want to emphasize that in my experience on iOS data-only messages will show up for you, the developer, while you are testing with your device plugged in in the app you use all the time (since you are the developer).

But iOS is very very very stingy with the power budget that it will give apps in response to messages, and if the device is too hot, low on battery at all, background refresh is off, or low power mode is on, or the user doesn't use your app all the time you will not get the FCM delivered to the iOS app as data-only

Most people opt for notification block on iOS for this reason

@OgDev-01
Copy link
Author

OgDev-01 commented Apr 8, 2025

interesting - usually it is the other way around, data-only works well for android in my experience but on ios they are problematic

I want to emphasize that in my experience on iOS data-only messages will show up for you, the developer, while you are testing with your device plugged in in the app you use all the time (since you are the developer).

But iOS is very very very stingy with the power budget that it will give apps in response to messages, and if the device is too hot, low on battery at all, background refresh is off, or low power mode is on, or the user doesn't use your app all the time you will not get the FCM delivered to the iOS app as data-only

Most people opt for notification block on iOS for this reason

Thank you for this insight... I'll keep a close watch on this behaviour and also try a POC on a separate app to see how opting to the notification block turns out on IOS.

Will update my findings here from time to time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Attention platform: android plugin: messaging FCM only - ( messaging() ) - do not use for Notifications type: bug New bug report
Projects
None yet
Development

No branches or pull requests

3 participants