diff --git a/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt b/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt index 97b58b1..57a1f42 100644 --- a/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt +++ b/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt @@ -10,9 +10,6 @@ import cloud.mindbox.mobile_sdk.logger.Level internal class MindboxEventEmitter( private val application: Application, ) : MindboxEventSubscriber { - - private var jsDelivery: MindboxJsDelivery? = null - override fun onEvent(event: MindboxSdkLifecycleEvent) { when (event) { is MindboxSdkLifecycleEvent.NewIntent -> handleNewIntent(event.reactContext, event.intent) @@ -25,7 +22,7 @@ internal class MindboxEventEmitter( Mindbox.writeLog("[RN] Handle new intent in event emitter. ", Level.INFO) Mindbox.onNewIntent(intent) Mindbox.onPushClicked(context, intent) - jsDelivery?.sendPushClicked(intent) + MindboxJsDelivery.sendPushClicked(intent) } private fun handleActivityCreated(reactContext: ReactContext, activity: Activity) { @@ -39,12 +36,9 @@ internal class MindboxEventEmitter( private fun initializeAndSendIntent(context: ReactContext, activity: Activity) { Mindbox.writeLog("[RN] Initialize MindboxJsDelivery", Level.INFO) - jsDelivery = MindboxJsDelivery.Shared.getInstance(context) val currentActivity: Activity = context.currentActivity ?: activity currentActivity.intent?.let { handleNewIntent(context, it) } } - private fun handleActivityDestroyed() { - jsDelivery = null - } + private fun handleActivityDestroyed() {} } diff --git a/android/src/main/java/com/mindboxsdk/MindboxJsDelivery.kt b/android/src/main/java/com/mindboxsdk/MindboxJsDelivery.kt index 981c3f1..a40f178 100644 --- a/android/src/main/java/com/mindboxsdk/MindboxJsDelivery.kt +++ b/android/src/main/java/com/mindboxsdk/MindboxJsDelivery.kt @@ -2,46 +2,39 @@ package com.mindboxsdk import android.content.Intent import android.os.Bundle -import com.facebook.react.bridge.ReactContext -import kotlin.properties.Delegates import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.logger.Level +import kotlin.properties.Delegates -class MindboxJsDelivery private constructor(private val mReactContext: ReactContext) { - companion object Shared { - private var INSTANCE: MindboxJsDelivery? = null - private var delayedIntent: Intent? = null - - var hasListeners: Boolean by Delegates.observable(false) { _, _, newValue -> - Mindbox.writeLog("[RN][MindboxJsDelivery] hasListeners=$newValue", Level.DEBUG) - if (newValue) { - delayedIntent?.let { intent -> - intent.extras?.let { - Mindbox.writeLog("[RN] Send push data from delayed ${it}", Level.INFO) - } - INSTANCE?.sendPushClicked(intent) - } - } - delayedIntent = null - } +object MindboxJsDelivery { + private var delayedIntent: Intent? = null - fun getInstance(reactContext: ReactContext): MindboxJsDelivery? { - if (INSTANCE == null) { - synchronized(MindboxJsDelivery::class.java) { - if (INSTANCE == null) { - INSTANCE = MindboxJsDelivery(reactContext) - } + internal var hasListeners: Boolean by Delegates.observable(false) { _, _, newValue -> + Mindbox.writeLog("[RN][MindboxJsDelivery] hasListeners=$newValue", Level.DEBUG) + if (newValue) { + delayedIntent?.let { intent -> + intent.extras?.let { + Mindbox.writeLog("[RN] Send push data from delayed ${it}", Level.INFO) } + sendPushClicked(intent) } - return INSTANCE } + delayedIntent = null } private fun sendEvent(eventName: String, bundle: Bundle) { Mindbox.writeLog("[RN][MindboxJsDelivery] sendEvent($eventName) push_url=${bundle.getString("push_url")}", Level.INFO) - MindboxSdkModule.deliverPushNotificationClickedFromExternal(bundle, mReactContext) + MindboxSdkModule.deliverPushNotificationClickedFromExternal(bundle) } + /** + * Sends a push-click intent to JS or delays it until listeners are registered. + * + * If no listeners are registered, the intent is cached and replayed later. Intents without + * `uniq_push_key` are ignored. + * + * @param intent push-click intent to process + */ fun sendPushClicked(intent: Intent) { if (hasListeners) { val bundle = intent.extras diff --git a/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt b/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt index 7d8798c..0453b12 100644 --- a/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt +++ b/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt @@ -5,7 +5,8 @@ import android.app.Application import android.content.Intent import android.os.Bundle import com.facebook.react.ReactApplication -import com.facebook.react.ReactInstanceManager +import com.facebook.react.ReactHost +import com.facebook.react.ReactInstanceEventListener import com.facebook.react.bridge.ActivityEventListener import com.facebook.react.bridge.ReactContext import java.util.concurrent.atomic.AtomicBoolean @@ -53,6 +54,7 @@ internal class MindboxSdkLifecycleListener private constructor( } private var activityEventListener: ActivityEventListener? = null + private var reactInstanceEventListener: ReactInstanceEventListener? = null private fun onReactContextAvailable(reactContext: ReactContext, activity: Activity) { Mindbox.writeLog("[RN] ReactContext ready", Level.INFO) @@ -60,31 +62,23 @@ internal class MindboxSdkLifecycleListener private constructor( subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityCreated(reactContext, activity)) } - private fun registerReactContextListener( - application: Application, - onReady: (ReactContext) -> Unit - ) { - val reactInstanceManager = getReactInstanceManager() - - val wrapperListener = object : ReactInstanceManager.ReactInstanceEventListener { - private val called = AtomicBoolean(false) + private fun registerReactContextListener(onReady: (ReactContext) -> Unit) { + val host = getReactHost(application) ?: run { + Mindbox.writeLog( + "[RN] registerReactContextListener: ReactHost is null, skip listener.", + Level.WARN + ) + return + } + reactInstanceEventListener?.let { host.removeReactInstanceEventListener(it) } + val listener = object : ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { - if (called.compareAndSet(false, true)) { - Mindbox.writeLog("[RN] ReactContext initialized (listener)", Level.INFO) - onReady(context) - } + Mindbox.writeLog("[RN] ReactContext initialized (listener)", Level.INFO) + onReady(context) } } - - reactInstanceManager?.addReactInstanceEventListener(wrapperListener) - // RN 0.78+ introduced ReactHost.addReactInstanceEventListener(...). - // Older RN versions (<= 0.74) expose only ReactInstanceManager.addReactInstanceEventListener(...). - // In New Architecture the ReactInstanceManager listener might not fire - // To support RN 0.78+ reliably while keeping backward compatibility, - // we try to register via ReactHost using reflection (no compile-time dependency). - // If ReactHost API is unavailable (older RN), this call is silently ignored and we rely on - // the ReactInstanceManager path. - addReactHostListener(application, wrapperListener) + reactInstanceEventListener = listener + host.addReactInstanceEventListener(listener) } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { @@ -92,7 +86,7 @@ internal class MindboxSdkLifecycleListener private constructor( val hasConsumedReactContext = AtomicBoolean(false) - registerReactContextListener(application) { reactContext -> + registerReactContextListener { reactContext -> if (hasConsumedReactContext.compareAndSet(false, true)) { onReactContextAvailable(reactContext, activity) } @@ -100,8 +94,8 @@ internal class MindboxSdkLifecycleListener private constructor( getReactContext()?.let { if (hasConsumedReactContext.compareAndSet(false, true)) { - onReactContextAvailable(it, activity) Mindbox.writeLog("[RN] ReactContext available (pre-existing)", Level.INFO) + onReactContextAvailable(it, activity) } } } @@ -136,6 +130,10 @@ internal class MindboxSdkLifecycleListener private constructor( subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityDestroyed(activity)) getReactContext()?.removeActivityEventListener(activityEventListener) activityEventListener = null + reactInstanceEventListener?.let { listener -> + getReactHost(application)?.removeReactInstanceEventListener(listener) + } + reactInstanceEventListener = null } override fun onActivityStarted(activity: Activity) {} @@ -144,68 +142,10 @@ internal class MindboxSdkLifecycleListener private constructor( override fun onActivityStopped(activity: Activity) {} override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + private fun getReactHost(application: Application): ReactHost? = + (application as? ReactApplication)?.reactHost - private fun getReactInstanceManager(): ReactInstanceManager? = - runCatching { - application.getReactApplication() - ?.reactNativeHost - ?.reactInstanceManager - }.onFailure { - Mindbox.writeLog("[RN] Bridgeless: ReactInstanceManager unsupported. Fallback to ReactHost", Level.INFO) - }.getOrNull() - - private fun Application.getReactApplication() = this as? ReactApplication - - private fun getReactContext(): ReactContext? { - return getReactInstanceManagerContext() ?: getReactHostContext(application) - } - - private fun getReactInstanceManagerContext(): ReactContext? { - return getReactInstanceManager()?.currentReactContext - } - - private fun getReactHostContext(application: Application): ReactContext? = - runCatching { - // RN 0.78+ moves reactContext from reactNativeHost.reactInstanceManager to reactHost - val reactApplication = application as ReactApplication - val getHostMethod = reactApplication.javaClass.getMethod("getReactHost") - val reactHost = getHostMethod.invoke(reactApplication) - val getContextMethod = reactHost.javaClass.getMethod("getCurrentReactContext") - getContextMethod.invoke(reactHost) as? ReactContext - }.onFailure { - Mindbox.writeLog("[RN] ReactHost currentReactContext unavailable", Level.INFO) - }.getOrNull() - - private fun addReactHostListener( - application: Application, - wrapperListener: ReactInstanceManager.ReactInstanceEventListener - ) { - runCatching { - val reactApplication = application as ReactApplication - - val hostClass = Class.forName("com.facebook.react.ReactHost") - val listenerClass = Class.forName("com.facebook.react.ReactInstanceEventListener") - - val addMethod = hostClass.getMethod("addReactInstanceEventListener", listenerClass) - val getHostMethod = reactApplication.javaClass.getMethod("getReactHost") - val reactHost = getHostMethod.invoke(reactApplication) - - val proxy = java.lang.reflect.Proxy.newProxyInstance( - listenerClass.classLoader, - arrayOf(listenerClass) - ) { _, method, args -> - if (method.name == "onReactContextInitialized" && args?.size == 1 && args[0] is ReactContext) { - wrapperListener.onReactContextInitialized(args[0] as ReactContext) - } - null - } - - addMethod.invoke(reactHost, proxy) - Mindbox.writeLog("[RN] success added react context listener for reactHost", Level.INFO) - }.onFailure { - Mindbox.writeLog("[RN] failed added react context listener for reactHost ", Level.ERROR) - } - } + private fun getReactContext(): ReactContext? = getReactHost(application)?.currentReactContext } /** diff --git a/android/src/main/java/com/mindboxsdk/MindboxSdkModule.kt b/android/src/main/java/com/mindboxsdk/MindboxSdkModule.kt index 6714903..570c31b 100644 --- a/android/src/main/java/com/mindboxsdk/MindboxSdkModule.kt +++ b/android/src/main/java/com/mindboxsdk/MindboxSdkModule.kt @@ -42,7 +42,7 @@ class MindboxSdkModule( } } - fun deliverPushNotificationClickedFromExternal(bundle: Bundle, fallbackReactContext: ReactContext) { + internal fun deliverPushNotificationClickedFromExternal(bundle: Bundle) { val module: MindboxSdkModule? = activeModule if (module != null) { module.emitPushFromDelivery(bundle) @@ -220,7 +220,7 @@ class MindboxSdkModule( } override fun onPushClickedIsRegistered(isRegistered: Boolean) { - MindboxJsDelivery.Shared.hasListeners = isRegistered + MindboxJsDelivery.hasListeners = isRegistered } override fun setLogLevel(level: Double) { diff --git a/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt b/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt index 6c212ef..d80cad8 100644 --- a/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt +++ b/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt @@ -1,37 +1,20 @@ package com.exampleapp -import android.content.Context import android.content.Intent import android.os.Bundle import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.logger.Level import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate -import com.facebook.react.ReactHost -import com.facebook.react.ReactInstanceManager -import com.facebook.react.bridge.ReactContext import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate import com.mindboxsdk.MindboxJsDelivery class MainActivity : ReactActivity() { - - private var jsDelivery: MindboxJsDelivery? = null - - private fun sendIntent(context: Context, intent: Intent) { + private fun handlePushIntent(intent: Intent) { Mindbox.onNewIntent(intent) - Mindbox.onPushClicked(context, intent) - jsDelivery?.sendPushClicked(intent) - } - - private fun initializeAndSentIntent(context: ReactContext) { - jsDelivery = MindboxJsDelivery.Shared.getInstance(context) - - if (context.hasCurrentActivity()) { - sendIntent(context, context.currentActivity!!.intent) - } else { - sendIntent(context, this.getIntent()) - } + Mindbox.onPushClicked(applicationContext, intent) + MindboxJsDelivery.sendPushClicked(intent) } override fun getMainComponentName(): String = "exampleApp" @@ -43,42 +26,14 @@ class MainActivity : ReactActivity() { super.onCreate(savedInstanceState) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { Mindbox.writeLog("[RN][exampleApp] New arch enabled", Level.DEBUG) - val reactHost: ReactHost = reactActivityDelegate.reactHost - val context: ReactContext? = reactHost.currentReactContext as? ReactContext - if (context != null) { - Mindbox.writeLog("[RN][exampleApp] ReactContext available on start", Level.DEBUG) - initializeAndSentIntent(context) - } else { - reactHost.addReactInstanceEventListener(object : - ReactInstanceManager.ReactInstanceEventListener { - override fun onReactContextInitialized(context: ReactContext) { - Mindbox.writeLog("[RN][exampleApp] ReactContext available on listener", Level.DEBUG) - initializeAndSentIntent(context) - reactHost.removeReactInstanceEventListener(this) - } - }) - } } else { Mindbox.writeLog("[RN][exampleApp] Old architecture", Level.DEBUG) - val reactInstanceManager: ReactInstanceManager = getReactNativeHost().reactInstanceManager - val reactContext: ReactContext? = reactInstanceManager.getCurrentReactContext() - if (reactContext != null) { - initializeAndSentIntent(reactContext) - Mindbox.writeLog("[RN][exampleApp] ReactContext available on start", Level.DEBUG) - } else { - reactInstanceManager.addReactInstanceEventListener(object : - ReactInstanceManager.ReactInstanceEventListener { - override fun onReactContextInitialized(context: ReactContext) { - initializeAndSentIntent(context) - reactInstanceManager.removeReactInstanceEventListener(this) - } - }) - } } + handlePushIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - sendIntent(this, intent) + handlePushIntent(intent) } }