invertase / react-native-firebase

πŸ”₯ A well-tested feature-rich modular Firebase implementation for React Native. Supports both iOS & Android platforms for all Firebase services.
https://rnfirebase.io
Other
11.55k stars 2.19k forks source link

[πŸ›] πŸ”₯ @react-native-firebase/messaging (Android) - Messaging() gets the wrong ReactContext when there is another Activity instantiated by ReactInstanceManager? #7768

Open longb1997 opened 2 months ago

longb1997 commented 2 months ago

Issue

Hello, first of all, thank you for the wonderful library. In my app chat. I have an "Bubble Chat" which uses Bubbles API. It will show Bubbles when it receives new notifications. And when you press on the bubble, then will display a window that runs startReactApplication display conversation screen (like Messenger).

The problem is that after pressing the Bubble, messaging() function in my Main App code doesn't works anymore, but if I use these codes in Bubble Chat code (separate from MainApp code) it still works. But if i delete Main App from Recent Apps, then reopen, from now on it work fine until I press a new bubble => start new conversation screen. (Ah sh*t, here we go again) => So i think when a react app start, this lib will use the most recent ReactContext of the app that just started.

Since messaging() in Main App was not working, it caused all my messaging().onNotificationOpenedApp, messaging().onMessage, messaging().getInitialNotification() to not working anymore Thanks for helping me

i will provide some code here:

Project Files

BubbleActivity.kt:

class BubbleActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler, PermissionAwareActivity {

    private lateinit var reactRootView: ReactRootView
    private lateinit var reactInstanceManager: ReactInstanceManager
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(null)
        SoLoader.init(this, false)

        val channelId = intent.getStringExtra("channelId")
        val initialProps = Bundle()
        if (channelId != null) {
            initialProps.putString("channelId", channelId)
        }

        reactRootView = ReactRootView(this)
        val packages: List<ReactPackage> = PackageList(application).packages
        // Packages that cannot be autolinked yet can be added manually here, for example:
        // packages.add(MyReactNativePackage())
        // Remember to include them in `settings.gradle` and `app/build.gradle` too.
            reactInstanceManager = ReactInstanceManager.builder()
                .setApplication(application)
                .setCurrentActivity(this)
                .setBundleAssetName("index2.android.bundle") => This Bubble has seperated code with MainApp, and it bundle will be generated before build.
                .setJSMainModulePath("index2") => entry file seperated with MainApp(index.js)
                .addPackages(packages)
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setJavaScriptExecutorFactory(HermesExecutorFactory())
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build()

        reactRootView.startReactApplication(reactInstanceManager, "chathead", initialProps) => It will start an ReactApp after pressed Bubble
        setContentView(reactRootView)
    }

    override fun onNewIntent(intent: Intent) {
        setIntent(intent)
        super.onNewIntent(intent)
    }
    override fun invokeDefaultOnBackPressed() {
        onBackPressedDispatcher.onBackPressed()
    }

    override fun onPause() {
        super.onPause()
        reactInstanceManager.onHostPause(this)
    }

    override fun onResume() {
        super.onResume()
        reactInstanceManager.onHostResume(this, this)
    }

    override fun onDestroy() {
        super.onDestroy()
        reactInstanceManager.onHostDestroy(this)
        reactRootView.unmountReactApplication()
    }

    override fun onBackPressed() {
        reactInstanceManager.onBackPressed()
        onBackPressedDispatcher.onBackPressed()
    }
}

And this is now in MainApp, where i use messaging() for handle notification

NotificationHandler.tsx

const NotificationHandler = memo(
  () => {
    // action press notify when app in background or active

    useEffect(() => {
      let unsubscribeOnNotificationOpenedApp: any = null;
      let unsubscribe: any = null;
      setTimeout(() => {
        unsubscribeOnNotificationOpenedApp = messaging().onNotificationOpenedApp(value => {
          return _.debounce(handleMessage, 300)(value);
        });

        unsubscribe = messaging().onMessage(message => {
          return require('lodash').debounce(handleRemoteMessage, 300)(message);
        });
      }, 1000);

      if (Platform.OS === 'android') {
        messaging()
          .getInitialNotification()
          .then(value => {
            return require('lodash').debounce(handleGetInitNotification, 1500)(value);
          });
      }

      return () => {
        unsubscribe && unsubscribe();
        unsubscribeOnNotificationOpenedApp && unsubscribeOnNotificationOpenedApp();
      };
    }, []);
    return null;
  },
  () => true
)

Then i used it like a component in MainApp

App.tsx

<>
        <NotificationHandler />
        <Routes />
</>

package.json:

    "react-native": "0.71.13",
    "@react-native-firebase/analytics": "^18.3.0",
    "@react-native-firebase/app": "^18.6.1",
    "@react-native-firebase/crashlytics": "^18.3.0",
    "@react-native-firebase/messaging": "^18.6.1",

Android

Click To Expand

#### Have you converted to AndroidX? - [x] my application is an AndroidX application? - [x] I am using `android/gradle.settings` `jetifier=true` for Android compatibility? - [x] I am using the NPM package `jetifier` for react-native compatibility? #### `android/build.gradle`: ```groovy buildscript { ext { buildToolsVersion = "33.0.0" minSdkVersion = 23 compileSdkVersion = 33 targetSdkVersion = 33 // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. ndkVersion = "23.1.7779620" playServicesLocationVersion = "21.0.1" kotlinVersion = '1.8.0' kotlin_version = '1.8.0' } repositories { google() mavenCentral() maven { url "https://jitpack.io" } } dependencies { classpath("com.android.tools.build:gradle:7.3.1") classpath("com.facebook.react:react-native-gradle-plugin") // Make sure that you have the Google services Gradle plugin dependency classpath("com.google.gms:google-services:4.3.15") classpath('com.google.firebase:firebase-crashlytics-gradle:2.9.9') classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } ``` #### `android/app/build.gradle`: ```groovy apply plugin: "com.android.application" apply plugin: "com.google.gms.google-services" // <- Add this line apply plugin: "com.facebook.react" apply plugin: 'kotlin-android' import com.android.build.OutputFile /** * 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 { /* 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") // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js // cliFile = file("../node_modules/react-native/cli.js") /* 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 command to run when bundling. By default is 'bundle' // bundleCommand = "ram-bundle" bundleCommand = "webpack-bundle" // 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"] } /** * Set this to true to create four separate APKs instead of one, * one for each native architecture. This is useful if you don't * use App Bundles (https://developer.android.com/guide/app-bundle/) * and want to have separate APKs to upload to the Play Store. */ def enableSeparateBuildPerCPUArchitecture = false /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ def enableProguardInReleaseBuilds = true /** * 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:+' def localPropertiesFile = rootProject.file('local.properties') def localProperties = new Properties() localProperties.load(new FileInputStream(localPropertiesFile)) /** * Private function to get the list of Native Architectures you want to build. * This reads the value from reactNativeArchitectures in your gradle.properties * file and works together with the --active-arch-only flag of react-native run-android. */ def reactNativeArchitectures() { def value = project.getProperties().get("reactNativeArchitectures") return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } android { ndkVersion rootProject.ext.ndkVersion compileSdkVersion rootProject.ext.compileSdkVersion namespace "com.example.app" defaultConfig { applicationId "com.example.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 8409223 versionName "7.3.3" multiDexEnabled true buildConfigField("String", "SECRET_KEY", localProperties["SECRET_KEY"]) } // for react-native-twilio-video-webrtc dexOptions { jumboMode true } splits { abi { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK include (*reactNativeArchitectures()) } } signingConfigs { debug { storeFile file('debug.keystore') storePassword 'android' keyAlias 'androiddebugkey' keyPassword 'android' } } // compileOptions { // sourceCompatibility JavaVersion.VERSION_17 // targetCompatibility JavaVersion.VERSION_17 // } 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 minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } // applicationVariants are e.g. debug, release applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // https://developer.android.com/studio/build/configure-apk-splits.html // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = defaultConfig.versionCode * 1000 + versionCodes.get(abi) } } } } dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation 'com.google.android.play:core:1.10.2' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") // for firebase // Import the BoM for the Firebase platform implementation(platform("com.google.firebase:firebase-bom:32.5.0")) // for HW keyboard implementation project(':react-native-hw-keyboard-event') implementation 'androidx.constraintlayout:constraintlayout:2.1.4' debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { exclude group:'com.squareup.okhttp3', module:'okhttp' } debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { implementation jscFlavor } // add kotlin version implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21" implementation "androidx.core:core-ktx:1.8.0" // more // If your app supports Android versions before Ice Cream Sandwich (API level 14) implementation 'com.facebook.fresco:animated-base-support:1.3.0' implementation 'com.facebook.fresco:fresco:2.6.0' // For animated GIF support implementation 'com.facebook.fresco:animated-gif:2.5.0' // For WebP support, including animated WebP implementation 'com.facebook.fresco:animated-webp:2.5.0' implementation 'com.facebook.fresco:webpsupport:2.5.0' // For WebP support, without animations implementation 'com.facebook.fresco:webpsupport:2.5.0' // for react-natie-video implementation "androidx.appcompat:appcompat:1.6.1" // using for geo location services implementation 'com.google.android.gms:play-services-location:21.0.1' implementation "androidx.cardview:cardview:1.0.0" implementation 'com.github.bumptech.glide:glide:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' ``` #### `android/settings.gradle`: ```groovy rootProject.name = 'chatapp' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':react-native-hw-keyboard-event' project(':react-native-hw-keyboard-event').projectDir = new File(settingsDir, '../node_modules/react-native-hw-keyboard-event/android') include ':app' includeBuild('../node_modules/react-native-gradle-plugin') ``` #### `MainApplication.java`: ```java package com.example.app; import android.app.Application; import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.defaults.DefaultReactNativeHost; import com.facebook.soloader.SoLoader; import java.util.List; import cl.json.ShareApplication; import org.wonday.orientation.OrientationActivityLifecycle; public class MainApplication extends Application implements ReactApplication, ShareApplication { private final ReactNativeHost mReactNativeHost = new DefaultReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); packages.add(new BubblePackage()); return packages; } @Override protected String getJSMainModuleName() { return "index"; } @Override protected boolean isNewArchEnabled() { return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; } @Override protected Boolean isHermesEnabled() { return BuildConfig.IS_HERMES_ENABLED; } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance()); SoLoader.init(this, /* native exopackage */ false); if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. DefaultNewArchitectureEntryPoint.load(); } ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); } @Override public String getFileProviderAuthority() { return "com.example.app"; } } ``` #### `AndroidManifest.xml`: ```xml ```


Environment

Click To Expand

**`react-native info` output:** ``` System: OS: macOS 14.4.1 CPU: (4) x64 Intel(R) Core(TM) i3-8100 CPU @ 3.60GHz Memory: 25.89 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 18.20.2 - /usr/local/bin/node Yarn: 1.22.22 - /usr/local/bin/yarn npm: 10.5.0 - /usr/local/bin/npm Watchman: 2024.04.15.00 - /usr/local/bin/watchman Managers: CocoaPods: 1.15.2 - /usr/local/lib/ruby/gems/3.3.0/bin/pod SDKs: iOS SDK: Platforms: DriverKit 23.4, iOS 17.4, macOS 14.4, tvOS 17.4, visionOS 1.1, watchOS 10.4 Android SDK: Not Found IDEs: Android Studio: 2023.2 AI-232.10300.40.2321.11567975 Xcode: 15.3/15E204a - /usr/bin/xcodebuild Languages: Java: 17.0.11 - /usr/bin/javac npmPackages: @react-native-community/cli: Not Found react: 18.2.0 => 18.2.0 react-native: 0.71.13 => 0.71.13 react-native-macos: Not Found npmGlobalPackages: *react-native*: Not Found``` - **Platform that you're experiencing the issue on**: - [ ] iOS - [x ] 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:** "@react-native-firebase/analytics": "^18.3.0", "@react-native-firebase/app": "^18.6.1", "@react-native-firebase/crashlytics": "^18.3.0", "@react-native-firebase/messaging": "^18.6.1", - **`Firebase` module(s) you're using that has the issue:** "@react-native-firebase/messaging": "^18.6.1", - **Are you using `TypeScript`?** - YES


longb1997 commented 2 months ago

If you need me to provide more information, feel free to ask me @mikehardy Thank you so much

mikehardy commented 2 months ago

Hey there - two thoughts,

1- can you use current versions please, just to make sure we're not chasing ghosts? 2- version skew within react-native-firebase packages violates our versioning expectations --> https://invertase.io/blog/react-native-firebase-versioning - all the same all the time is the rule

    "react-native": "0.71.13",
    "@react-native-firebase/analytics": "^18.3.0",
    "@react-native-firebase/app": "^18.6.1",
    "@react-native-firebase/crashlytics": "^18.3.0",
    "@react-native-firebase/messaging": "^18.6.1",
mikehardy commented 2 months ago

For troubleshooting:

So i think when a react app start, this lib will use the most recent ReactContext of the app that just started.

What makes you think that? I suggest using System.out.println or similar in the native code (yours and you can add it easily to react-native-firebase/messaging) to log out exactly what context is in use to see if they are different and/or incorrect.

In general it sounds like inference on a problem but without direct proof, and direct proof / ability to fix + test fixes will be difficult in the context of a large app whereas a https://stackoverflow.com/help/minimal-reproducible-example that was purpose built to expose the issue in could make it trivial to spot + verify fix

longb1997 commented 2 months ago

For troubleshooting:

So i think when a react app start, this lib will use the most recent ReactContext of the app that just started.

What makes you think that? I suggest using System.out.println or similar in the native code (yours and you can add it easily to react-native-firebase/messaging) to log out exactly what context is in use to see if they are different and/or incorrect.

In general it sounds like inference on a problem but without direct proof, and direct proof / ability to fix + test fixes will be difficult in the context of a large app whereas a https://stackoverflow.com/help/minimal-reproducible-example that was purpose built to expose the issue in could make it trivial to spot + verify fix

I think so because if I copy notificationHandler's code and use in Bubble code, messages() works properly in Bubble code

MainApp uses index.js entry file, Bubble uses index2.js entry file, each file leads to its own folder in the source code

Edit: I put Log in onReceive inside ReactNativeFirebaseMessagingReceiver.java, it is the same context. But I don't know why messaging() code only runs in Bubble Code after opening the Bubble screen. If I close Bubble and close the app in recent apps. Then use it normally without open Bubble it works normally

longb1997 commented 2 months ago

Hey there - two thoughts,

1- can you use current versions please, just to make sure we're not chasing ghosts? 2- version skew within react-native-firebase packages violates our versioning expectations --> https://invertase.io/blog/react-native-firebase-versioning - all the same all the time is the rule

    "react-native": "0.71.13",
    "@react-native-firebase/analytics": "^18.3.0",
    "@react-native-firebase/app": "^18.6.1",
    "@react-native-firebase/crashlytics": "^18.3.0",
    "@react-native-firebase/messaging": "^18.6.1",

my bad, thanks for your reply. I just updated all React-native-firebase to the latest version (19.2.2). But the result is still the same.

longb1997 commented 2 months ago

<activity android:name=".BubbleActivity" android:resizeableActivity="true" android:allowEmbedded="true" tools:targetApi="n" />

I updated BubbleActivity in AndroidManifest, when I open MainApp without closing Bubble, it looks like messaging() "thinks" MainApp is not in foreground

mikehardy commented 2 months ago

In general it sounds like inference on a problem but without direct proof, and direct proof / ability to fix + test fixes will be difficult in the context of a large app whereas a https://stackoverflow.com/help/minimal-reproducible-example that was purpose built to expose the issue in could make it trivial to spot + verify fix

longb1997 commented 2 months ago

In general it sounds like inference on a problem but without direct proof, and direct proof / ability to fix + test fixes will be difficult in the context of a large app whereas a https://stackoverflow.com/help/minimal-reproducible-example that was purpose built to expose the issue in could make it trivial to spot + verify fix

Hi there, Sorry for my interpretation, as you said, it's difficult to spot and verify fix using only my description So I created a mini-repo to reproduce this error, please take a look https://github.com/longb1997/rn-fcm-issue-7768 Thank you for your time, I really appreciate it

longb1997 commented 2 months ago

if you have any confusion, please tell me @mikehardy Thank you very much

decisionnguyen commented 2 months ago

Same issue :(

longb1997 commented 1 month ago

@mikehardy thanks for labeling it, do you plan to fix this issue in the future? p/s: I'm not urging you, I'm just posting a comment to avoid bots closing this post because it's inactive xD Thank you

longb1997 commented 2 weeks ago

Hi, do you have any updates on this (or plans?)