Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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() {}
}
47 changes: 20 additions & 27 deletions android/src/main/java/com/mindboxsdk/MindboxJsDelivery.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment thread
sergeysozinov marked this conversation as resolved.

/**
* 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
Expand Down
112 changes: 26 additions & 86 deletions android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,55 +54,48 @@ 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)
addActivityEventListener(reactContext)
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
Comment thread
sergeysozinov marked this conversation as resolved.
}
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)
Comment thread
sergeysozinov marked this conversation as resolved.
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (!isMainActivity(activity)) return

val hasConsumedReactContext = AtomicBoolean(false)

registerReactContextListener(application) { reactContext ->
registerReactContextListener { reactContext ->
if (hasConsumedReactContext.compareAndSet(false, true)) {
onReactContextAvailable(reactContext, activity)
}
}

getReactContext()?.let {
if (hasConsumedReactContext.compareAndSet(false, true)) {
onReactContextAvailable(it, activity)
Mindbox.writeLog("[RN] ReactContext available (pre-existing)", Level.INFO)
onReactContextAvailable(it, activity)
}
}
}
Expand Down Expand Up @@ -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) {}
Expand All @@ -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
Comment thread
sergeysozinov marked this conversation as resolved.

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
}

/**
Expand Down
4 changes: 2 additions & 2 deletions android/src/main/java/com/mindboxsdk/MindboxSdkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class MindboxSdkModule(
}
}

fun deliverPushNotificationClickedFromExternal(bundle: Bundle, fallbackReactContext: ReactContext) {
internal fun deliverPushNotificationClickedFromExternal(bundle: Bundle) {
Comment thread
sergeysozinov marked this conversation as resolved.
val module: MindboxSdkModule? = activeModule
if (module != null) {
module.emitPushFromDelivery(bundle)
Expand Down Expand Up @@ -220,7 +220,7 @@ class MindboxSdkModule(
}

override fun onPushClickedIsRegistered(isRegistered: Boolean) {
MindboxJsDelivery.Shared.hasListeners = isRegistered
MindboxJsDelivery.hasListeners = isRegistered
}

override fun setLogLevel(level: Double) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
}
}
Loading