From 39b7582342468bb1fc4c899c671d3f6090be4016 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Wed, 22 Jul 2026 17:20:27 -0400 Subject: [PATCH 1/9] feat(android): support mParticle Android SDK 6 Update the Android bridge to compile against mParticle Android SDK 6 and the Rokt kit APIs while keeping the existing React Native Rokt JavaScript surface. The Expo config plugin and sample now use bounded 6.x Android kit ranges, and migration docs describe the SDK and build requirements for app developers. --- ExpoTestApp/README.md | 7 +- MIGRATING.md | 29 ++++++- README.md | 9 +- android/build.gradle | 8 +- .../com/mparticle/react/MParticleModule.kt | 27 +++--- .../com/mparticle/react/MParticlePackage.kt | 66 ++++++++++----- .../mparticle/react/rokt/MPRoktModuleImpl.kt | 83 +++++++------------ .../react/rokt/RoktLayoutViewManagerImpl.kt | 13 +-- .../com/mparticle/react/rokt/MPRoktModule.kt | 8 +- .../react/rokt/RoktLayoutViewManager.kt | 5 +- .../com/mparticle/react/rokt/MPRoktModule.kt | 8 +- .../react/rokt/RoktLayoutViewManager.kt | 2 +- js/rokt/rokt.ts | 42 ++++++++-- plugin/src/withMParticleAndroid.ts | 6 +- sample/README.md | 8 +- sample/android/app/build.gradle | 4 +- 16 files changed, 184 insertions(+), 141 deletions(-) diff --git a/ExpoTestApp/README.md b/ExpoTestApp/README.md index 7c5c250c..0dd08634 100644 --- a/ExpoTestApp/README.md +++ b/ExpoTestApp/README.md @@ -99,8 +99,9 @@ The Rokt section also demonstrates: - Using `RoktLayoutView` as an embedded placeholder component On Android, the Rokt session APIs require `android-core` and -`android-rokt-kit` `5.79.0` or newer. If configured, the shared Expo -`customBaseUrl` setting is applied to Android through +`android-rokt-kit` `6.0.0` or newer. Apps that include `android-rokt-kit` +`6.0.0` must build with `compileSdk` 35+ and Android Gradle Plugin 8.6+. If +configured, the shared Expo `customBaseUrl` setting is applied to Android through `NetworkOptions.setCustomBaseURL`. ### Implementation guide: Shoppable Ads (`selectShoppableAds`) @@ -262,7 +263,7 @@ Check `android/app/build.gradle` for kit dependencies (if specified): ```gradle dependencies { // mParticle kits - implementation "com.mparticle:android-rokt-kit:+" + implementation "com.mparticle:android-rokt-kit:[6.0.0, 7.0)" } ``` diff --git a/MIGRATING.md b/MIGRATING.md index 1cbbb449..eddff73e 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -1,7 +1,32 @@ # Migration Guides This document provides migration guidance for changes in `react-native-mparticle`. -Release versions and changelog entries are generated by the Release Draft workflow. + +## Migrating Android apps to mParticle Android SDK 6.0.0 + +The React Native JavaScript API is unchanged. Existing calls to +`MParticle.Rokt.selectPlacements`, `close()`, `setSessionId()`, +`getSessionId()`, and `purchaseFinalized()` continue to use the same +JavaScript signatures and event names. + +### Android Rokt Dependencies + +Android apps that use Rokt must include matching 6.x mParticle artifacts: + +```gradle +implementation "com.mparticle:android-core:[6.0.0, 7.0)" +implementation "com.mparticle:android-rokt-kit:[6.0.0, 7.0)" +``` + +Apps that include `android-rokt-kit` `6.0.0` must build with `compileSdk` 35+ +and Android Gradle Plugin 8.6+. + +### Android Expo Config Plugin + +If you use the Expo config plugin with `androidKits`, the plugin injects kit +dependencies with the `[6.0.0, 7.0)` range. + +`selectShoppableAds` remains a no-op on Android in this release. ## Migrating to the mParticle Apple SDK 9.2.0 Rokt update @@ -32,7 +57,7 @@ MParticle.Rokt.getSessionId(): Promise `close()` is supported on iOS and Android. Session APIs are backed by the iOS mParticle Rokt kit. On Android, apps that use these session APIs must use -`android-core` and `android-rokt-kit` `5.79.0` or newer. +`android-core` and `android-rokt-kit` `6.0.0` or newer. ### Expo Config Plugin diff --git a/README.md b/README.md index 20d1e664..58b9f2a0 100644 --- a/README.md +++ b/README.md @@ -301,10 +301,11 @@ In Expo apps, use `iosKits: ["mParticle-Rokt"]` for standard Rokt placements. Th See [MIGRATING.md](./MIGRATING.md) for release-specific migration guidance. -For Android integrations that use `MParticle.Rokt.setSessionId()` or -`MParticle.Rokt.getSessionId()`, `android-core` and `android-rokt-kit` -`5.79.0` or newer are required. Android CNAME setup through -`customBaseUrl` also requires `android-core` `5.79.0` or newer. +For Android Rokt integrations, including `MParticle.Rokt.*` APIs and +`RoktLayoutView`, `android-core` and `android-rokt-kit` `6.0.0` or newer are +required. Apps that include `android-rokt-kit` `6.0.0` must build with +`compileSdk` 35+ and Android Gradle Plugin 8.6+. Android CNAME setup through +`customBaseUrl` also requires `android-core` `6.0.0` or newer. See [Identity](http://docs.mparticle.com/developers/sdk/ios/identity/) for more information on supplying an `MPIdentityApiRequest` object during SDK initialization. diff --git a/android/build.gradle b/android/build.gradle index 01f72d96..cfcb65a7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -118,9 +118,8 @@ dependencies { // // (See https://github.com/mparticle/mparticle-android-sdk for the latest version) // - // Bounded upper bound prevents resolving to 6.0.0-rc.1+ on Maven Central. - // 6.x removed deprecated symbols (e.g. UserAttributeListener); see #710. - api 'com.mparticle:android-core:[5.79.2, 6.0)' + api 'com.mparticle:android-core:[6.0.0, 7.0)' + compileOnly 'com.mparticle:android-rokt-kit:[6.0.0, 7.0)' // // And, if you want to include kits, you can do so as follows: @@ -136,6 +135,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation files('libs/java-json.jar') - testImplementation 'com.mparticle:android-core:5.79.2' + testImplementation 'com.mparticle:android-core:6.0.0' + testCompileOnly 'com.mparticle:android-rokt-kit:6.0.0' testImplementation("com.facebook.react:react-android:+") } diff --git a/android/src/main/java/com/mparticle/react/MParticleModule.kt b/android/src/main/java/com/mparticle/react/MParticleModule.kt index 2eaf6ee3..70877cad 100644 --- a/android/src/main/java/com/mparticle/react/MParticleModule.kt +++ b/android/src/main/java/com/mparticle/react/MParticleModule.kt @@ -14,7 +14,7 @@ import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import com.mparticle.MPEvent import com.mparticle.MParticle -import com.mparticle.UserAttributeListener +import com.mparticle.TypedUserAttributeListener import com.mparticle.commerce.CommerceEvent import com.mparticle.commerce.Impression import com.mparticle.commerce.Product @@ -153,22 +153,21 @@ class MParticleModule( val selectedUser = MParticle.getInstance()?.Identity()?.getUser(parseMpid(mpid)) if (selectedUser != null) { selectedUser.getUserAttributes( - object : UserAttributeListener { + object : TypedUserAttributeListener { override fun onUserAttributesReceived( - userAttributes: Map?, - userAttributeLists: Map>?, - mpid: Long?, + userAttributes: Map, + userAttributeLists: Map?>, + mpid: Long, ) { - val resultMap = WritableNativeMap() - userAttributes?.let { attrs -> - for ((key, value) in attrs) { - resultMap.putString(key, value) - } + val resultMap = getWritableMap() + for ((key, value) in userAttributes) { + resultMap.putString(key, value?.toString()) } - userAttributeLists?.let { attrLists -> - for ((key, valueList) in attrLists) { + + for ((key, valueList) in userAttributeLists) { + valueList?.let { val resultArray = WritableNativeArray() - for (arrayVal in valueList) { + for (arrayVal in it) { resultArray.pushString(arrayVal) } resultMap.putArray(key, resultArray) @@ -179,7 +178,7 @@ class MParticleModule( }, ) } else { - callback.invoke(null, WritableNativeMap()) + callback.invoke(null, getWritableMap()) } } diff --git a/android/src/main/java/com/mparticle/react/MParticlePackage.kt b/android/src/main/java/com/mparticle/react/MParticlePackage.kt index b6f412b7..8b7a77d2 100644 --- a/android/src/main/java/com/mparticle/react/MParticlePackage.kt +++ b/android/src/main/java/com/mparticle/react/MParticlePackage.kt @@ -8,7 +8,6 @@ import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import com.mparticle.react.rokt.MPRoktModule -import com.mparticle.react.rokt.MPRoktModuleImpl import com.mparticle.react.rokt.RoktLayoutViewManager class MParticlePackage : TurboReactPackage() { @@ -21,14 +20,23 @@ class MParticlePackage : TurboReactPackage() { MParticleModule(reactContext) } - MPRoktModuleImpl.MODULE_NAME -> { - MPRoktModule(reactContext) + ROKT_MODULE_NAME -> { + if (isRoktKitAvailable()) { + MPRoktModule(reactContext) + } else { + null + } } else -> null } - override fun createViewManagers(reactContext: ReactApplicationContext): List> = listOf(RoktLayoutViewManager()) + override fun createViewManagers(reactContext: ReactApplicationContext): List> = + if (isRoktKitAvailable()) { + listOf(RoktLayoutViewManager()) + } else { + emptyList() + } override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { @@ -46,23 +54,43 @@ class MParticlePackage : TurboReactPackage() { BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, // isTurboModule ), ) - moduleInfos.put( - MPRoktModuleImpl.MODULE_NAME, - ReactModuleInfo( - MPRoktModuleImpl.MODULE_NAME, - MPRoktModuleImpl.MODULE_NAME, - true, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, // isTurboModule - ), - ) + if (isRoktKitAvailable()) { + moduleInfos.put( + ROKT_MODULE_NAME, + ReactModuleInfo( + ROKT_MODULE_NAME, + ROKT_MODULE_NAME, + true, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, // isTurboModule + ), + ) + } moduleInfos.toMap() } override fun getViewManagers(reactContext: ReactApplicationContext): List = - listOf( - ModuleSpec.viewManagerSpec { RoktLayoutViewManager() }, - ) + if (isRoktKitAvailable()) { + listOf( + ModuleSpec.viewManagerSpec { RoktLayoutViewManager() }, + ) + } else { + emptyList() + } + + private fun isRoktKitAvailable(): Boolean = + try { + Class.forName("com.mparticle.kits.RoktEmbeddedView") + true + } catch (_: ClassNotFoundException) { + false + } catch (_: LinkageError) { + false + } + + private companion object { + const val ROKT_MODULE_NAME = "RNMPRokt" + } } diff --git a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt index e13f8f1b..f6b61825 100644 --- a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt @@ -13,13 +13,12 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.mparticle.MParticle -import com.mparticle.MpRoktEventCallback -import com.mparticle.RoktEvent -import com.mparticle.UnloadReasons import com.mparticle.WrapperSdk import com.mparticle.internal.Logger -import com.mparticle.rokt.CacheConfig -import com.mparticle.rokt.RoktConfig +import com.mparticle.kits.rokt +import com.rokt.roktsdk.CacheConfig +import com.rokt.roktsdk.RoktConfig +import com.rokt.roktsdk.RoktEvent import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -31,13 +30,7 @@ class MPRoktModuleImpl( MParticle.getInstance()?.setWrapperSdk(WrapperSdk.WrapperSdkReactNative, "") } - private var roktEventHandler: MpRoktEventCallback? = null - private val eventSubscriptions = mutableMapOf() - private val listeners: MutableMap = - object : LinkedHashMap() { - override fun removeEldestEntry(eldest: Map.Entry): Boolean = this.size > MAX_LISTENERS - } fun getName(): String = MODULE_NAME @@ -54,11 +47,11 @@ class MPRoktModuleImpl( catalogItemId: String, success: Boolean, ) { - MParticle.getInstance()?.Rokt()?.purchaseFinalized(placementId, catalogItemId, success) + MParticle.getInstance()?.rokt?.purchaseFinalized(placementId, catalogItemId, success) } fun close(promise: Promise) { - MParticle.getInstance()?.Rokt()?.close() + MParticle.getInstance()?.rokt?.close() promise.resolve(null) } @@ -66,44 +59,17 @@ class MPRoktModuleImpl( sessionId: String, promise: Promise, ) { - MParticle.getInstance()?.Rokt()?.setSessionId(sessionId) + MParticle.getInstance()?.rokt?.setSessionId(sessionId) promise.resolve(null) } fun getSessionId(promise: Promise) { - promise.resolve(MParticle.getInstance()?.Rokt()?.getSessionId()) - } - - fun setRoktEventHandler(roktEventHandler: MpRoktEventCallback) { - this.roktEventHandler = roktEventHandler - } - - fun createRoktCallback(): MpRoktEventCallback { - val callback: MpRoktEventCallback = - object : MpRoktEventCallback { - override fun onLoad() { - sendCallback("onLoad", null) - } - - override fun onUnload(reason: UnloadReasons) { - sendCallback("onUnLoad", reason.toString()) - } - - override fun onShouldShowLoadingIndicator() { - sendCallback("onShouldShowLoadingIndicator", null) - } - - override fun onShouldHideLoadingIndicator() { - sendCallback("onShouldHideLoadingIndicator", null) - } - } - listeners[System.currentTimeMillis()] = callback - return callback + promise.resolve(MParticle.getInstance()?.rokt?.getSessionId()) } fun sendCallback( eventValue: String, - reason: String?, + reason: String? = null, ) { val params = Arguments.createMap() params.putString("callbackValue", eventValue) @@ -153,7 +119,11 @@ class MPRoktModuleImpl( } val cacheAttributes = if (cacheConfigMap?.hasKey("cacheAttributes") == true) { - cacheConfigMap.getMap("cacheAttributes")?.toHashMap()?.mapValues { it.value as String } + cacheConfigMap + .getMap("cacheAttributes") + ?.toHashMap() + ?.mapNotNull { (key, value) -> (value as? String)?.let { key to it } } + ?.toMap() } else { null } @@ -182,51 +152,55 @@ class MPRoktModuleImpl( when (event) { is RoktEvent.FirstPositiveEngagement -> { eventName = "FirstPositiveEngagement" - event.placementId + event.identifier } RoktEvent.HideLoadingIndicator -> { eventName = "HideLoadingIndicator" + sendCallback("onShouldHideLoadingIndicator") null } is RoktEvent.OfferEngagement -> { eventName = "OfferEngagement" - event.placementId + event.identifier } is RoktEvent.PlacementClosed -> { eventName = "PlacementClosed" - event.placementId + sendCallback("onUnLoad") + event.identifier } is RoktEvent.PlacementCompleted -> { eventName = "PlacementCompleted" - event.placementId + event.identifier } is RoktEvent.PlacementFailure -> { eventName = "PlacementFailure" - event.placementId + event.identifier } is RoktEvent.PlacementInteractive -> { eventName = "PlacementInteractive" - event.placementId + event.identifier } is RoktEvent.PlacementReady -> { eventName = "PlacementReady" - event.placementId + sendCallback("onLoad") + event.identifier } is RoktEvent.PositiveEngagement -> { eventName = "PositiveEngagement" - event.placementId + event.identifier } RoktEvent.ShowLoadingIndicator -> { eventName = "ShowLoadingIndicator" + sendCallback("onShouldShowLoadingIndicator") null } @@ -239,7 +213,7 @@ class MPRoktModuleImpl( is RoktEvent.OpenUrl -> { eventName = "OpenUrl" params.putString("url", event.url) - event.placementId + event.identifier } is RoktEvent.CartItemInstantPurchase -> { @@ -252,7 +226,7 @@ class MPRoktModuleImpl( params.putDouble("totalPrice", event.totalPrice) params.putInt("quantity", event.quantity) params.putDouble("unitPrice", event.unitPrice) - event.placementId + event.identifier } else -> { @@ -272,7 +246,6 @@ class MPRoktModuleImpl( } companion object { - const val MAX_LISTENERS = 5 const val MODULE_NAME = "RNMPRokt" } } diff --git a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt index d6ab5e5d..9362775b 100644 --- a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt @@ -4,8 +4,8 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.events.RCTEventEmitter -import com.mparticle.rokt.RoktEmbeddedView -import com.mparticle.rokt.RoktLayoutDimensionCallBack +import com.mparticle.kits.RoktEmbeddedView +import com.mparticle.kits.RoktLayoutDimensionCallBack class RoktLayoutViewManagerImpl { companion object { @@ -35,15 +35,6 @@ class RoktLayoutViewManagerImpl { override fun onHeightChanged(height: Int) { changeHeight(widget.context as ReactContext, height, widget.id) } - - override fun onMarginChanged( - start: Int, - top: Int, - end: Int, - bottom: Int, - ) { - changeMargin(widget.context as ReactContext, widget.id, start, top, end, bottom) - } } } diff --git a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt index d6617cf8..2112e62e 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -9,8 +9,9 @@ import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.UIManagerHelper import com.mparticle.MParticle import com.mparticle.internal.Logger +import com.mparticle.kits.RoktEmbeddedView +import com.mparticle.kits.rokt import com.mparticle.react.NativeMPRoktSpec -import com.mparticle.rokt.RoktEmbeddedView import java.lang.ref.WeakReference import java.util.concurrent.CountDownLatch @@ -33,7 +34,7 @@ class MPRoktModule( Logger.warning("selectPlacements failed. identifier cannot be empty") return } - MParticle.getInstance()?.Rokt()?.events(identifier)?.let { + MParticle.getInstance()?.rokt?.events(identifier)?.let { impl.startRoktEventListener(it, reactContext.currentActivity, identifier) } @@ -41,10 +42,9 @@ class MPRoktModule( val placeholdersMap = processPlaceholders(placeholders) val config = roktConfig?.let { impl.buildRoktConfig(it) } - MParticle.getInstance()?.Rokt()?.selectPlacements( + MParticle.getInstance()?.rokt?.selectPlacements( identifier = identifier, attributes = impl.readableMapToMapOfStrings(attributes), - callbacks = impl.createRoktCallback(), embeddedViews = placeholdersMap, fontTypefaces = null, // TODO config = config, diff --git a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt index 96c430fb..cece5867 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt @@ -4,9 +4,10 @@ import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.viewmanagers.RoktNativeLayoutManagerInterface -import com.mparticle.rokt.RoktEmbeddedView +import com.mparticle.kits.RoktEmbeddedView -class RoktLayoutViewManager : SimpleViewManager(), +class RoktLayoutViewManager : + SimpleViewManager(), RoktNativeLayoutManagerInterface { private val impl = RoktLayoutViewManagerImpl() diff --git a/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt index ad005c30..c1000641 100644 --- a/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -8,8 +8,9 @@ import com.facebook.react.uimanager.NativeViewHierarchyManager import com.facebook.react.uimanager.UIManagerModule import com.mparticle.MParticle import com.mparticle.internal.Logger +import com.mparticle.kits.RoktEmbeddedView +import com.mparticle.kits.rokt import com.mparticle.react.NativeMPRoktSpec -import com.mparticle.rokt.RoktEmbeddedView import java.lang.ref.WeakReference class MPRoktModule( @@ -32,16 +33,15 @@ class MPRoktModule( return } val uiManager = reactContext.getNativeModule(UIManagerModule::class.java) - MParticle.getInstance()?.Rokt()?.events(identifier)?.let { + MParticle.getInstance()?.rokt?.events(identifier)?.let { impl.startRoktEventListener(it, reactContext.currentActivity, identifier) } val config = roktConfig?.let { impl.buildRoktConfig(it) } uiManager?.addUIBlock { nativeViewHierarchyManager -> - MParticle.getInstance()?.Rokt()?.selectPlacements( + MParticle.getInstance()?.rokt?.selectPlacements( identifier = identifier, attributes = impl.readableMapToMapOfStrings(attributes), - callbacks = impl.createRoktCallback(), embeddedViews = safeUnwrapPlaceholders(placeholders, nativeViewHierarchyManager), fontTypefaces = null, // TODO config = config, diff --git a/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt b/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt index 9e2d1446..702c551d 100644 --- a/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt +++ b/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt @@ -3,7 +3,7 @@ package com.mparticle.react.rokt import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager -import com.mparticle.rokt.RoktEmbeddedView +import com.mparticle.kits.RoktEmbeddedView class RoktLayoutViewManager : ViewGroupManager() { private val impl = RoktLayoutViewManagerImpl() diff --git a/js/rokt/rokt.ts b/js/rokt/rokt.ts index fc4faf06..908e26f8 100644 --- a/js/rokt/rokt.ts +++ b/js/rokt/rokt.ts @@ -1,8 +1,32 @@ -import { NativeModules } from 'react-native'; -import { getNativeModule } from '../utils/architecture'; +import { NativeModules, Platform, TurboModuleRegistry } from 'react-native'; +import { getNativeModule, isNewArchitecture } from '../utils/architecture'; import type { Spec as NativeMPRoktInterface } from '../codegenSpecs/rokt/NativeMPRokt'; -const MPRokt = getNativeModule('RNMPRokt'); +const ROKT_MODULE_NAME = 'RNMPRokt'; +const MPRokt = + Platform.OS === 'android' + ? getAndroidRoktModule() + : getNativeModule(ROKT_MODULE_NAME); + +function getAndroidRoktModule(): NativeMPRoktInterface | null { + if (isNewArchitecture) { + return TurboModuleRegistry.get(ROKT_MODULE_NAME); + } + return ( + (NativeModules[ROKT_MODULE_NAME] as NativeMPRoktInterface | undefined) ?? + null + ); +} + +function getMPRokt(): NativeMPRoktInterface { + if (MPRokt != null) { + return MPRokt; + } + + throw new Error( + `${ROKT_MODULE_NAME} is unavailable. Add the native mParticle Rokt kit before using MParticle.Rokt APIs.` + ); +} export abstract class Rokt { /** @@ -22,7 +46,7 @@ export abstract class Rokt { roktConfig?: IRoktConfig, fontFilesMap?: Record ): Promise { - MPRokt.selectPlacements( + getMPRokt().selectPlacements( identifier, attributes, placeholders, @@ -36,7 +60,7 @@ export abstract class Rokt { attributes: Record, roktConfig?: IRoktConfig ): Promise { - MPRokt.selectShoppableAds(identifier, attributes, roktConfig); + getMPRokt().selectShoppableAds(identifier, attributes, roktConfig); } static async purchaseFinalized( @@ -44,19 +68,19 @@ export abstract class Rokt { catalogItemId: string, success: boolean ): Promise { - MPRokt.purchaseFinalized(placementId, catalogItemId, success); + getMPRokt().purchaseFinalized(placementId, catalogItemId, success); } static close(): Promise { - return MPRokt.close(); + return getMPRokt().close(); } static setSessionId(sessionId: string): Promise { - return MPRokt.setSessionId(sessionId); + return getMPRokt().setSessionId(sessionId); } static getSessionId(): Promise { - return MPRokt.getSessionId(); + return getMPRokt().getSessionId(); } static createRoktConfig(colorMode?: ColorMode, cacheConfig?: CacheConfig) { diff --git a/plugin/src/withMParticleAndroid.ts b/plugin/src/withMParticleAndroid.ts index fb399ca9..b9eae92c 100644 --- a/plugin/src/withMParticleAndroid.ts +++ b/plugin/src/withMParticleAndroid.ts @@ -381,11 +381,9 @@ const withMParticleAppBuildGradle: ConfigPlugin = ( // Generate kit dependency lines // Bounded range matches the core SDK range in android/build.gradle so the - // kit and core stay paired on a 5.x line. An unbounded `+` would resolve - // to a pre-release (e.g. 6.0.0-rc.1) and transitively drag the core past - // the bridge's compiled-against API surface. + // kit and core stay paired on a supported 6.x line. const kitDependencies = props.androidKits - .map(kit => ` implementation "com.mparticle:${kit}:[5.79.2, 6.0)"`) + .map(kit => ` implementation "com.mparticle:${kit}:[6.0.0, 7.0)"`) .join('\n'); // Use mergeContents for idempotent injection diff --git a/sample/README.md b/sample/README.md index c76fe779..76723aab 100644 --- a/sample/README.md +++ b/sample/README.md @@ -67,9 +67,11 @@ pod install The sample Podfile pins `mParticle-Rokt` `~> 9.2`, **`Rokt-Widget` `5.2.0`**, and **`DcuiSchema` `2.7.0`**. Rokt’s pods allow `DcuiSchema` to float within `~> 2.6`; when CocoaPods resolves **2.8.0+**, the schema adds `image` styling that can desync from the `RoktUXHelper` sources in that widget line and break Swift compile (`StyleTransformer` / `BaseStyles`). Bump these pins together when you adopt a newer Rokt iOS stack. The sample Android app pins `com.mparticle:android-core` and -`com.mparticle:android-rokt-kit` to `5.79.2` so the Rokt session APIs and -Android CNAME support are available. Payment-extension installation and native -URL callback forwarding are not configured in this release. +`com.mparticle:android-rokt-kit` to `[6.0.0, 7.0)` so the Rokt session APIs and +Android CNAME support are available. Apps that include `android-rokt-kit` +`6.0.0` must build with `compileSdk` 35+ and Android Gradle Plugin 8.6+; the +sample uses `compileSdk` 36. Payment-extension installation and native URL +callback forwarding are not configured in this release. ## Running the Sample App diff --git a/sample/android/app/build.gradle b/sample/android/app/build.gradle index 31b65748..840eb6f1 100644 --- a/sample/android/app/build.gradle +++ b/sample/android/app/build.gradle @@ -110,8 +110,8 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("com.mparticle:android-core:5.79.2") - implementation("com.mparticle:android-rokt-kit:5.79.2") + implementation("com.mparticle:android-core:[6.0.0, 7.0)") + implementation("com.mparticle:android-rokt-kit:[6.0.0, 7.0)") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") From 9384beab6f3d21709a3159c7ed6a782b34db27cf Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 10:59:09 -0400 Subject: [PATCH 2/9] fix(android): dispatch Rokt layout height via EventDispatcher, not RCTEventEmitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Rokt embedded layout view manager emitted its height-changed event through `context.getJSModule(RCTEventEmitter).receiveEvent(...)`. Under the New Architecture bridgeless runtime (default on RN >= 0.76, the wrapper's floor) this logs an `IllegalArgumentException` SoftException on every height change and stops working entirely once the Fabric interop layer is disabled — which would drop the height event and leave embedded placements mis-sized. Replace it with `UIManagerHelper.getEventDispatcherForReactTag` + an `Event` subclass overriding `getEventData()`. This is the RN-recommended path and works on both Paper and Fabric across all supported RN versions, from a single `main/` implementation (no arch/version branching). Also remove the dead margin path (`changeMargin`, `EVENT_MARGIN_CHANGED`, `onLayoutMarginChanged`, `MarginChangedEvent`): mParticle Android SDK 6's `RoktLayoutDimensionCallBack` only exposes `onHeightChanged`, so nothing ever fired the margin event. iOS never emitted it either (height flows via the separate RoktEventManager emitter), so dropping it from the shared codegen spec is safe on both platforms. Verified: sample app builds, embedded placement renders to interactive at the correct height with no SoftException in logcat. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../react/rokt/RoktLayoutViewManagerImpl.kt | 41 ++++++++----------- .../react/rokt/RoktLayoutViewManager.kt | 3 -- .../rokt/RoktLayoutNativeComponent.ts | 8 ---- js/rokt/rokt-layout-view.android.tsx | 39 ------------------ js/rokt/rokt-layout-view.ios.tsx | 24 ----------- 5 files changed, 18 insertions(+), 97 deletions(-) diff --git a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt index 9362775b..d3367227 100644 --- a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt @@ -2,8 +2,10 @@ package com.mparticle.react.rokt import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.events.RCTEventEmitter +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event import com.mparticle.kits.RoktEmbeddedView import com.mparticle.kits.RoktLayoutDimensionCallBack @@ -11,7 +13,6 @@ class RoktLayoutViewManagerImpl { companion object { const val REACT_CLASS = "RoktNativeLayout" const val EVENT_HEIGHT_CHANGED = "onLayoutHeightChanged" - const val EVENT_MARGIN_CHANGED = "onLayoutMarginChanged" } fun getName(): String = REACT_CLASS @@ -43,28 +44,22 @@ class RoktLayoutViewManagerImpl { height: Int, id: Int, ) { - val event = Arguments.createMap() - event.putString("height", height.toString()) - context - .getJSModule(RCTEventEmitter::class.java) - .receiveEvent(id, EVENT_HEIGHT_CHANGED, event) + // Use the EventDispatcher API instead of getJSModule(RCTEventEmitter), which is + // unsupported under the New Architecture bridgeless runtime (RN >= 0.76). This path + // works on both Paper and Fabric across all supported RN versions. + val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id) ?: return + val surfaceId = UIManagerHelper.getSurfaceId(context) + dispatcher.dispatchEvent(OnLayoutHeightChangedEvent(surfaceId, id, height)) } - fun changeMargin( - context: ReactContext, - id: Int, - start: Int, - top: Int, - end: Int, - bottom: Int, - ) { - val event = Arguments.createMap() - event.putString("marginLeft", start.toString()) - event.putString("marginTop", top.toString()) - event.putString("marginRight", end.toString()) - event.putString("marginBottom", bottom.toString()) - context - .getJSModule(RCTEventEmitter::class.java) - .receiveEvent(id, EVENT_MARGIN_CHANGED, event) + private class OnLayoutHeightChangedEvent( + surfaceId: Int, + viewId: Int, + private val height: Int, + ) : Event(surfaceId, viewId) { + override fun getEventName(): String = RoktLayoutViewManagerImpl.EVENT_HEIGHT_CHANGED + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { putString("height", height.toString()) } } } diff --git a/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt b/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt index 702c551d..a425fd2f 100644 --- a/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt +++ b/android/src/oldarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt @@ -18,9 +18,6 @@ class RoktLayoutViewManager : ViewGroupManager() { .put( RoktLayoutViewManagerImpl.EVENT_HEIGHT_CHANGED, MapBuilder.of("registrationName", RoktLayoutViewManagerImpl.EVENT_HEIGHT_CHANGED), - ).put( - RoktLayoutViewManagerImpl.EVENT_MARGIN_CHANGED, - MapBuilder.of("registrationName", RoktLayoutViewManagerImpl.EVENT_MARGIN_CHANGED), ).build() override fun needsCustomLayoutForChildren(): Boolean = false diff --git a/js/codegenSpecs/rokt/RoktLayoutNativeComponent.ts b/js/codegenSpecs/rokt/RoktLayoutNativeComponent.ts index 740c29e3..b0b6309c 100644 --- a/js/codegenSpecs/rokt/RoktLayoutNativeComponent.ts +++ b/js/codegenSpecs/rokt/RoktLayoutNativeComponent.ts @@ -7,13 +7,6 @@ type HeightChangedEvent = { height: string; }; -type MarginChangedEvent = { - marginTop: string; - marginRight: string; - marginLeft: string; - marginBottom: string; -}; - // Native component props - these are the props the native component expects export interface NativeProps extends ViewProps { // Placeholder name to be passed to the native component @@ -21,7 +14,6 @@ export interface NativeProps extends ViewProps { // Custom events onLayoutHeightChanged?: DirectEventHandler; - onLayoutMarginChanged?: DirectEventHandler; } export default codegenNativeComponent( diff --git a/js/rokt/rokt-layout-view.android.tsx b/js/rokt/rokt-layout-view.android.tsx index f6f6d6f9..b06cad39 100644 --- a/js/rokt/rokt-layout-view.android.tsx +++ b/js/rokt/rokt-layout-view.android.tsx @@ -16,10 +16,6 @@ export interface RoktLayoutViewProps { export interface RoktLayoutViewState { height: number; placeholderName: string; - marginTop: number; - marginRight: number; - marginLeft: number; - marginBottom: number; } /** @@ -31,15 +27,6 @@ interface HeightChangedEvent { }; } -interface MarginChangedEvent { - nativeEvent: { - marginTop?: string; - marginLeft?: string; - marginRight?: string; - marginBottom?: string; - }; -} - const styles = StyleSheet.create({ widget: { flex: 1, @@ -62,10 +49,6 @@ export class RoktLayoutView extends Component< this.state = { height: 0, placeholderName: this.props.placeholderName, - marginTop: 0, - marginRight: 0, - marginLeft: 0, - marginBottom: 0, }; } @@ -79,23 +62,6 @@ export class RoktLayoutView extends Component< } }; - /** - * Handles the margin changed event from the native component - * This is an internal implementation detail not exposed to users - */ - private handleMarginChanged = (event: MarginChangedEvent) => { - if (event && event.nativeEvent) { - const { marginTop, marginLeft, marginRight, marginBottom } = - event.nativeEvent; - this.setState({ - marginTop: parseInt(marginTop || '0'), - marginLeft: parseInt(marginLeft || '0'), - marginRight: parseInt(marginRight || '0'), - marginBottom: parseInt(marginBottom || '0'), - }); - } - }; - override render() { try { // Get the placeholderName from props @@ -114,14 +80,9 @@ export class RoktLayoutView extends Component< styles.widget, { height: this.state.height, - marginTop: this.state.marginTop, - marginLeft: this.state.marginLeft, - marginRight: this.state.marginRight, - marginBottom: this.state.marginBottom, }, ]} onLayoutHeightChanged={this.handleHeightChanged} - onLayoutMarginChanged={this.handleMarginChanged} /> ); } catch (error) { diff --git a/js/rokt/rokt-layout-view.ios.tsx b/js/rokt/rokt-layout-view.ios.tsx index fae46173..05754d92 100644 --- a/js/rokt/rokt-layout-view.ios.tsx +++ b/js/rokt/rokt-layout-view.ios.tsx @@ -16,13 +16,6 @@ export interface HeightChangedEvent extends Event { height: string; } -export interface MarginChangedEvent extends Event { - marginTop: string; - marginRight: string; - marginLeft: string; - marginBottom: string; -} - export interface WidgetChangeEvent { selectedPlacement: string; height: string; @@ -35,17 +28,12 @@ export interface RoktLayoutViewProps { export interface RoktLayoutViewState { height: number; placeholderName: string; - marginTop: number; - marginRight: number; - marginLeft: number; - marginBottom: number; } // Define the native component props interface interface RoktNativeLayoutProps extends ViewProps { placeholderName?: string; onLayoutHeightChanged?: (event: HeightChangedEvent) => void; - onLayoutMarginChanged?: (event: MarginChangedEvent) => void; } // Use the appropriate component based on architecture @@ -76,10 +64,6 @@ export class RoktLayoutView extends Component< this.state = { height: 0, placeholderName: this.props.placeholderName, - marginTop: 0, - marginRight: 0, - marginLeft: 0, - marginBottom: 0, }; } @@ -93,14 +77,6 @@ export class RoktLayoutView extends Component< this.setState({ height: parseInt(event.height) }); } }} - onLayoutMarginChanged={(event: MarginChangedEvent) => { - this.setState({ - marginTop: parseInt(event.marginTop || '0'), - marginRight: parseInt(event.marginRight || '0'), - marginLeft: parseInt(event.marginLeft || '0'), - marginBottom: parseInt(event.marginBottom || '0'), - }); - }} /> ); } From c47e2a4c995b0f20858e365899b5502d0bf23d2e Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 11:16:54 -0400 Subject: [PATCH 3/9] fix(android): override getDelegate on Fabric Rokt layout ViewManager Under the New Architecture, `RoktLayoutViewManager` implements the codegen-generated `RoktNativeLayoutManagerInterface` but never returned a delegate from `getDelegate()`. On every view creation Fabric calls `getOrCreateViewManagerDelegate()`, the base ViewManager returns null and logs `ReactNoCrashSoftException: ViewManager using codegen must override getDelegate method (name: RoktNativeLayout)`, and the `placeholderName` prop is not routed through the generated delegate. Instantiate `RoktNativeLayoutManagerDelegate` and return it from `getDelegate()`. The delegate's `setProperty` now owns prop dispatch, so the redundant `@ReactProp` annotation is removed. Explicit generic type arguments are supplied because Kotlin cannot infer the delegate's intersection-bounded type parameter. newarch source set only; the Paper `ViewGroupManager` path uses no codegen delegate and is unaffected. Verified: sample app builds, embedded placement renders to interactive with no getDelegate SoftException. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/mparticle/react/rokt/RoktLayoutViewManager.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt index cece5867..4f9bbc41 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt @@ -2,7 +2,8 @@ package com.mparticle.react.rokt import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RoktNativeLayoutManagerDelegate import com.facebook.react.viewmanagers.RoktNativeLayoutManagerInterface import com.mparticle.kits.RoktEmbeddedView @@ -11,12 +12,17 @@ class RoktLayoutViewManager : RoktNativeLayoutManagerInterface { private val impl = RoktLayoutViewManagerImpl() + // Fabric routes props through the codegen-generated delegate; without this override the + // base ViewManager logs "ViewManager using codegen must override getDelegate method". + private val delegate = RoktNativeLayoutManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = delegate + override fun getName(): String = impl.getName() override fun createViewInstance(reactContext: ThemedReactContext): RoktEmbeddedView = impl.createViewInstance(reactContext) - @ReactProp(name = "placeholderName") override fun setPlaceholderName(view: RoktEmbeddedView?, value: String?) { impl.setPlaceholderName(view, value) } From 74bd5239db4df011518e7182ed41c58274b2b089 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 13:25:38 -0400 Subject: [PATCH 4/9] fix(android): run Rokt selectPlacements and close on the main thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under the New Architecture, `MPRoktModule.selectPlacements` invoked the SDK's `rokt.selectPlacements(...)` on the TurboModule thread. mParticle Android SDK 6's `RoktKit.selectPlacements` clears prior placeholder content with `ViewGroup.removeAllViews()`, which detaches the now Compose-based Rokt widget and triggers `LifecycleRegistry.removeObserver` — hard-enforced to the main thread. Showing an embedded placement then another placement crashed with `IllegalStateException: Method removeObserver must be called on the main thread`. The bug was latent before SDK 6 (the old View-based widget tolerated the off-thread detach); the Compose migration turned it into a deterministic crash. Resolve placeholder views and call the SDK together inside a single `UiThreadUtil.runOnUiThread {}`, dropping the CountDownLatch (which also removed a blocking cross-thread wait on the module thread). Guard `close()` the same way, since `rokt.close()` detaches overlay Compose views. This matches the oldarch path (`UIManager.addUIBlock`) and iOS (uiManager methodQueue), both of which already dispatch on the main thread and were unaffected. Verified: embedded → overlay no longer crashes and both render to interactive; ROKT CLOSE dismisses the overlay (PlacementClosed) with no exception. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../mparticle/react/rokt/MPRoktModuleImpl.kt | 8 +- .../com/mparticle/react/rokt/MPRoktModule.kt | 112 ++++++++---------- 2 files changed, 54 insertions(+), 66 deletions(-) diff --git a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt index f6b61825..10ca238b 100644 --- a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt @@ -10,6 +10,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.mparticle.MParticle @@ -51,8 +52,11 @@ class MPRoktModuleImpl( } fun close(promise: Promise) { - MParticle.getInstance()?.rokt?.close() - promise.resolve(null) + // rokt.close() dismisses/detaches Compose overlay views, which must happen on the main thread. + UiThreadUtil.runOnUiThread { + MParticle.getInstance()?.rokt?.close() + promise.resolve(null) + } } fun setSessionId( diff --git a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt index 2112e62e..0f5b25f9 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -13,7 +13,6 @@ import com.mparticle.kits.RoktEmbeddedView import com.mparticle.kits.rokt import com.mparticle.react.NativeMPRoktSpec import java.lang.ref.WeakReference -import java.util.concurrent.CountDownLatch class MPRoktModule( private val reactContext: ReactApplicationContext, @@ -38,17 +37,22 @@ class MPRoktModule( impl.startRoktEventListener(it, reactContext.currentActivity, identifier) } - // Process placeholders for Fabric - val placeholdersMap = processPlaceholders(placeholders) val config = roktConfig?.let { impl.buildRoktConfig(it) } - - MParticle.getInstance()?.rokt?.selectPlacements( - identifier = identifier, - attributes = impl.readableMapToMapOfStrings(attributes), - embeddedViews = placeholdersMap, - fontTypefaces = null, // TODO - config = config, - ) + val attributeMap = impl.readableMapToMapOfStrings(attributes) + + // Rokt SDK 6's selectPlacements clears prior placeholder content via removeAllViews(), + // which detaches Compose views and must run on the main thread. Resolve the placeholder + // views and invoke the SDK together on the UI thread. (oldarch uses UIManager.addUIBlock; + // iOS uses the uiManager methodQueue — same intent.) + UiThreadUtil.runOnUiThread { + MParticle.getInstance()?.rokt?.selectPlacements( + identifier = identifier, + attributes = attributeMap, + embeddedViews = resolvePlaceholders(placeholders), + fontTypefaces = null, // TODO + config = config, + ) + } } @ReactMethod @@ -88,66 +92,46 @@ class MPRoktModule( } /** - * Process placeholders from ReadableMap to a map of Widgets for use with Rokt. - * This method handles the Fabric-specific view resolution. + * Resolve placeholders from a ReadableMap of react tags to their RoktEmbeddedView instances. + * Must be called on the UI thread — it resolves live views via the UIManager. */ - private fun processPlaceholders(placeholders: ReadableMap?): Map> { + private fun resolvePlaceholders(placeholders: ReadableMap?): Map> { val placeholdersMap = HashMap>() + if (placeholders == null) { + return placeholdersMap + } + + val iterator = placeholders.keySetIterator() + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + try { + val reactTag = + when { + placeholders.getType(key) == ReadableType.Number -> { + placeholders.getDouble(key).toInt() + } - if (placeholders != null) { - // Use CountDownLatch to wait for UI thread processing - val latch = CountDownLatch(1) - - // Run view resolution on UI thread - UiThreadUtil.runOnUiThread { - try { - val iterator = placeholders.keySetIterator() - while (iterator.hasNextKey()) { - val key = iterator.nextKey() - try { - // Get the tag value as an integer - val reactTag = - when { - placeholders.getType(key) == ReadableType.Number -> { - placeholders.getDouble(key).toInt() - } - - else -> { - Logger.warning("Invalid view tag for key: $key") - continue - } - } - - // Get the UIManager for this specific tag - val uiManager = - UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag) - if (uiManager == null) { - Logger.warning("UIManager not found for tag: $reactTag") - continue - } - - // Resolve the view using the manager (now on UI thread) - val view = uiManager.resolveView(reactTag) - if (view is RoktEmbeddedView) { - placeholdersMap[key] = WeakReference(view) - Logger.debug("Successfully found Widget for key: $key with tag: $reactTag") - } else { - Logger.warning("View with tag $reactTag is not a Widget: ${view?.javaClass?.simpleName}") - } - } catch (e: Exception) { - Logger.warning("Error processing placeholder for key $key: ${e.message}") + else -> { + Logger.warning("Invalid view tag for key: $key") + continue } } - } finally { - latch.countDown() + + val uiManager = UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag) + if (uiManager == null) { + Logger.warning("UIManager not found for tag: $reactTag") + continue } - } - try { - // Wait for UI thread to finish processing - latch.await() - } catch (e: InterruptedException) { - Logger.warning("Interrupted while waiting for UI thread: ${e.message}") + val view = uiManager.resolveView(reactTag) + if (view is RoktEmbeddedView) { + placeholdersMap[key] = WeakReference(view) + Logger.debug("Successfully found Widget for key: $key with tag: $reactTag") + } else { + Logger.warning("View with tag $reactTag is not a Widget: ${view?.javaClass?.simpleName}") + } + } catch (e: Exception) { + Logger.warning("Error processing placeholder for key $key: ${e.message}") } } From 9204e57e111c636af972f6be9378e699a52a0307 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 13:56:50 -0400 Subject: [PATCH 5/9] style(android): satisfy ktlint function-signature on Rokt layout view managers Join single-expression bodies onto their signatures (getEventData, createViewInstance) and wrap the multi-parameter setPlaceholderName signature, matching the repo's existing Kotlin style and clearing the standard:function-signature violations from ktlintMainSourceSetCheck. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt | 3 +-- .../com/mparticle/react/rokt/RoktLayoutViewManager.kt | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt index d3367227..d267b3fa 100644 --- a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt @@ -59,7 +59,6 @@ class RoktLayoutViewManagerImpl { ) : Event(surfaceId, viewId) { override fun getEventName(): String = RoktLayoutViewManagerImpl.EVENT_HEIGHT_CHANGED - override fun getEventData(): WritableMap = - Arguments.createMap().apply { putString("height", height.toString()) } + override fun getEventData(): WritableMap = Arguments.createMap().apply { putString("height", height.toString()) } } } diff --git a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt index 4f9bbc41..5f26fdd6 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt @@ -20,10 +20,12 @@ class RoktLayoutViewManager : override fun getName(): String = impl.getName() - override fun createViewInstance(reactContext: ThemedReactContext): RoktEmbeddedView = - impl.createViewInstance(reactContext) + override fun createViewInstance(reactContext: ThemedReactContext): RoktEmbeddedView = impl.createViewInstance(reactContext) - override fun setPlaceholderName(view: RoktEmbeddedView?, value: String?) { + override fun setPlaceholderName( + view: RoktEmbeddedView?, + value: String?, + ) { impl.setPlaceholderName(view, value) } } From 844f2a3a1aa6afb9fcdcc2d1deeba4130855e326 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 14:05:23 -0400 Subject: [PATCH 6/9] fix(rokt): make close/setSessionId/getSessionId async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These three methods are typed to return a Promise but called getMPRokt() synchronously, so a missing-native-module error threw synchronously instead of rejecting the returned Promise — inconsistent with the type signature and with the sibling methods (selectPlacements, selectShoppableAds, purchaseFinalized), which are already async. A caller using `.catch()` would miss the error. Mark them async so the getMPRokt() throw surfaces as a rejected Promise. Return types are unchanged, so this is non-breaking; native promise resolution is unaffected. Addresses copilot PR review comments on js/rokt/rokt.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- js/rokt/rokt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/rokt/rokt.ts b/js/rokt/rokt.ts index 908e26f8..f7eee054 100644 --- a/js/rokt/rokt.ts +++ b/js/rokt/rokt.ts @@ -71,15 +71,15 @@ export abstract class Rokt { getMPRokt().purchaseFinalized(placementId, catalogItemId, success); } - static close(): Promise { + static async close(): Promise { return getMPRokt().close(); } - static setSessionId(sessionId: string): Promise { + static async setSessionId(sessionId: string): Promise { return getMPRokt().setSessionId(sessionId); } - static getSessionId(): Promise { + static async getSessionId(): Promise { return getMPRokt().getSessionId(); } From 732b862e7c1644a06c7b4db4b56e2c3fe9f16d80 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 14:13:32 -0400 Subject: [PATCH 7/9] perf(android): cache Rokt kit availability check in MParticlePackage isRoktKitAvailable() ran a reflective Class.forName on each of its four call sites during module and view-manager registration at startup. The kit's presence on the classpath is fixed for the app's lifetime, so convert it to a `by lazy` val that performs the reflection once. Addresses copilot PR review comment on MParticlePackage.kt. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/mparticle/react/MParticlePackage.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/mparticle/react/MParticlePackage.kt b/android/src/main/java/com/mparticle/react/MParticlePackage.kt index 8b7a77d2..ed47df4e 100644 --- a/android/src/main/java/com/mparticle/react/MParticlePackage.kt +++ b/android/src/main/java/com/mparticle/react/MParticlePackage.kt @@ -21,7 +21,7 @@ class MParticlePackage : TurboReactPackage() { } ROKT_MODULE_NAME -> { - if (isRoktKitAvailable()) { + if (isRoktKitAvailable) { MPRoktModule(reactContext) } else { null @@ -32,7 +32,7 @@ class MParticlePackage : TurboReactPackage() { } override fun createViewManagers(reactContext: ReactApplicationContext): List> = - if (isRoktKitAvailable()) { + if (isRoktKitAvailable) { listOf(RoktLayoutViewManager()) } else { emptyList() @@ -54,7 +54,7 @@ class MParticlePackage : TurboReactPackage() { BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, // isTurboModule ), ) - if (isRoktKitAvailable()) { + if (isRoktKitAvailable) { moduleInfos.put( ROKT_MODULE_NAME, ReactModuleInfo( @@ -72,7 +72,7 @@ class MParticlePackage : TurboReactPackage() { } override fun getViewManagers(reactContext: ReactApplicationContext): List = - if (isRoktKitAvailable()) { + if (isRoktKitAvailable) { listOf( ModuleSpec.viewManagerSpec { RoktLayoutViewManager() }, ) @@ -80,7 +80,9 @@ class MParticlePackage : TurboReactPackage() { emptyList() } - private fun isRoktKitAvailable(): Boolean = + // The Rokt kit's presence on the classpath is fixed for the app's lifetime; compute the + // reflective check once instead of on every module/view-manager registration call. + private val isRoktKitAvailable: Boolean by lazy { try { Class.forName("com.mparticle.kits.RoktEmbeddedView") true @@ -89,6 +91,7 @@ class MParticlePackage : TurboReactPackage() { } catch (_: LinkageError) { false } + } private companion object { const val ROKT_MODULE_NAME = "RNMPRokt" From c85c290ad678caed103aca13154cb2b847845dac Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 14:22:53 -0400 Subject: [PATCH 8/9] docs(sample): clarify Android dependency pin range in README State that both com.mparticle:android-core and android-rokt-kit are pinned to [6.0.0, 7.0) (removes the ambiguity flagged in review) and drop the Android CNAME mention. Addresses copilot PR review comment on sample/README.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- sample/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sample/README.md b/sample/README.md index 76723aab..555f1349 100644 --- a/sample/README.md +++ b/sample/README.md @@ -66,12 +66,12 @@ pod install The sample Podfile pins `mParticle-Rokt` `~> 9.2`, **`Rokt-Widget` `5.2.0`**, and **`DcuiSchema` `2.7.0`**. Rokt’s pods allow `DcuiSchema` to float within `~> 2.6`; when CocoaPods resolves **2.8.0+**, the schema adds `image` styling that can desync from the `RoktUXHelper` sources in that widget line and break Swift compile (`StyleTransformer` / `BaseStyles`). Bump these pins together when you adopt a newer Rokt iOS stack. -The sample Android app pins `com.mparticle:android-core` and -`com.mparticle:android-rokt-kit` to `[6.0.0, 7.0)` so the Rokt session APIs and -Android CNAME support are available. Apps that include `android-rokt-kit` -`6.0.0` must build with `compileSdk` 35+ and Android Gradle Plugin 8.6+; the -sample uses `compileSdk` 36. Payment-extension installation and native URL -callback forwarding are not configured in this release. +The sample Android app pins both `com.mparticle:android-core` and +`com.mparticle:android-rokt-kit` to `[6.0.0, 7.0)` so the Rokt session APIs are +available. Apps that include `android-rokt-kit` `6.0.0` must build with +`compileSdk` 35+ and Android Gradle Plugin 8.6+; the sample uses `compileSdk` +36. Payment-extension installation and native URL callback forwarding are not +configured in this release. ## Running the Sample App From cc78cff93a764f50043969ec13b6244b9c2bba22 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Thu, 23 Jul 2026 14:34:36 -0400 Subject: [PATCH 9/9] docs(sample): fix README markdown formatting (prettier) Rewrap the Android dependency paragraph so a line no longer begins with "36." (which prettier/markdown would treat as an ordered-list item), satisfying the trunk prettier check. Co-Authored-By: Claude Opus 4.8 (1M context) --- sample/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sample/README.md b/sample/README.md index 555f1349..7890f181 100644 --- a/sample/README.md +++ b/sample/README.md @@ -69,8 +69,7 @@ The sample Podfile pins `mParticle-Rokt` `~> 9.2`, **`Rokt-Widget` `5.2.0`**, an The sample Android app pins both `com.mparticle:android-core` and `com.mparticle:android-rokt-kit` to `[6.0.0, 7.0)` so the Rokt session APIs are available. Apps that include `android-rokt-kit` `6.0.0` must build with -`compileSdk` 35+ and Android Gradle Plugin 8.6+; the sample uses `compileSdk` -36. Payment-extension installation and native URL callback forwarding are not +`compileSdk` 35+ and Android Gradle Plugin 8.6+; the sample uses `compileSdk` 36. Payment-extension installation and native URL callback forwarding are not configured in this release. ## Running the Sample App