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..ed47df4e 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,46 @@ 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() + } + + // 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 + } 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..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,16 +10,16 @@ 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 -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 +31,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,56 +48,32 @@ 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() - 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( 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 +123,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 +156,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 +217,7 @@ class MPRoktModuleImpl( is RoktEvent.OpenUrl -> { eventName = "OpenUrl" params.putString("url", event.url) - event.placementId + event.identifier } is RoktEvent.CartItemInstantPurchase -> { @@ -252,7 +230,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 +250,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..d267b3fa 100644 --- a/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/RoktLayoutViewManagerImpl.kt @@ -2,16 +2,17 @@ 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.mparticle.rokt.RoktEmbeddedView -import com.mparticle.rokt.RoktLayoutDimensionCallBack +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event +import com.mparticle.kits.RoktEmbeddedView +import com.mparticle.kits.RoktLayoutDimensionCallBack 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 @@ -35,15 +36,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) - } } } @@ -52,28 +44,21 @@ 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/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt index d6617cf8..0f5b25f9 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -9,10 +9,10 @@ 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 class MPRoktModule( private val reactContext: ReactApplicationContext, @@ -33,22 +33,26 @@ 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) } - // 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), - callbacks = impl.createRoktCallback(), - 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}") } } 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..5f26fdd6 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/RoktLayoutViewManager.kt @@ -2,21 +2,30 @@ 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.rokt.RoktEmbeddedView +import com.mparticle.kits.RoktEmbeddedView -class RoktLayoutViewManager : SimpleViewManager(), +class RoktLayoutViewManager : + SimpleViewManager(), 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) + override fun createViewInstance(reactContext: ThemedReactContext): RoktEmbeddedView = impl.createViewInstance(reactContext) - @ReactProp(name = "placeholderName") - override fun setPlaceholderName(view: RoktEmbeddedView?, value: String?) { + override fun setPlaceholderName( + view: RoktEmbeddedView?, + value: String?, + ) { impl.setPlaceholderName(view, value) } } 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..a425fd2f 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() @@ -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'), - }); - }} /> ); } diff --git a/js/rokt/rokt.ts b/js/rokt/rokt.ts index fc4faf06..f7eee054 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(); + static async close(): Promise { + return getMPRokt().close(); } - static setSessionId(sessionId: string): Promise { - return MPRokt.setSessionId(sessionId); + static async setSessionId(sessionId: string): Promise { + return getMPRokt().setSessionId(sessionId); } - static getSessionId(): Promise { - return MPRokt.getSessionId(); + static async getSessionId(): Promise { + 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..7890f181 100644 --- a/sample/README.md +++ b/sample/README.md @@ -66,10 +66,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. +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 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")