From 7cb972b104e020752cf6a330fbcec71d43c0ff62 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Tue, 26 May 2026 14:27:43 -0400 Subject: [PATCH] feat(rokt): add kit API parity access Add Kotlin MParticle.rokt access so partners can call Rokt kit APIs from the active mParticle instance again. Forward native Rokt SDK Compose events through the kit RoktLayout wrapper and document the Kotlin and Java entry points. --- CHANGELOG.md | 3 + kits/rokt/rokt/README.md | 36 ++++++++++- .../com/mparticle/kits/MParticleRokt.kt | 29 +++++++-- .../kotlin/com/mparticle/kits/RoktLayout.kt | 14 +++++ .../kotlin/com/mparticle/kits/RoktTest.kt | 61 +++++++++++++++++++ 5 files changed, 137 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aefd8fcbc..fcfa57e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ + + ## [Unreleased] ### Changed - Add support for qualified alpha, beta, and release candidate versions in release workflows. +- Add Kotlin `MParticle.rokt` access and `RoktLayout` event callbacks for the Rokt kit. ### Removed diff --git a/kits/rokt/rokt/README.md b/kits/rokt/rokt/README.md index efddbbcd5..072d4b31e 100644 --- a/kits/rokt/rokt/README.md +++ b/kits/rokt/rokt/README.md @@ -8,13 +8,47 @@ This repository contains the [Rokt](https://docs.rokt.com/) integration for the ```groovy dependencies { - implementation 'com.mparticle:android-rokt-kit:5+' + implementation 'com.mparticle:android-rokt-kit:6+' } ``` 2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Rokt detected"` in the output of `adb logcat`. 3. Reference mParticle's integration docs below to enable the integration. +## Usage + +Kotlin consumers can access the Rokt Kit facade from the mParticle instance: + +```kotlin +import com.mparticle.MParticle +import com.mparticle.kits.rokt + +MParticle.getInstance()?.rokt?.selectPlacements( + identifier = "RoktExperience", + attributes = attributes, +) +``` + +Java consumers can use the kit helper: + +```java +MParticleRokt.Rokt().selectPlacements("RoktExperience", attributes); +``` + +Compose integrations can receive native Rokt SDK events from `RoktLayout`: + +```kotlin +RoktLayout( + sdkTriggered = true, + identifier = "RoktExperience", + attributes = attributes, + location = "RoktEmbedded1", + onEvent = { event -> + // Handle RoktEvent + }, +) +``` + ## Documentation [Rokt integration](https://docs.rokt.com/developers/integration-guides/android/overview) diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/MParticleRokt.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/MParticleRokt.kt index 12a079a93..841d5ae84 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/MParticleRokt.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/MParticleRokt.kt @@ -9,6 +9,9 @@ object MParticleRokt { @Volatile private var rokt: Rokt? = null + @Volatile + private var roktInstance: MParticle? = null + @Suppress("FunctionName") @JvmStatic fun Rokt(): Rokt { @@ -16,16 +19,32 @@ object MParticleRokt { "MParticle must be started before calling MParticleRokt.Rokt()" } - synchronized(this) { - rokt?.let { return it } + return roktFor(mParticle) + } + + internal fun roktFor(mParticle: MParticle): Rokt = synchronized(this) { + val existing = rokt + if (existing != null && roktInstance === mParticle) { + return existing + } - return createRokt(mParticle).also { - rokt = it - } + return createRokt(mParticle).also { + rokt = it + roktInstance = mParticle } } } +/** + * Returns the Rokt Kit facade bound to this mParticle instance. + * + * Kotlin consumers can use this property to call Rokt Kit APIs through + * `MParticle.getInstance()?.rokt`. Java consumers should continue to use + * [MParticleRokt.Rokt]. + */ +val MParticle.rokt: Rokt + get() = MParticleRokt.roktFor(this) + private fun createRokt(mParticle: MParticle): Rokt { val kitManager = mParticle.Internal().kitManager return Rokt(kitManager) diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktLayout.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktLayout.kt index 895ae5306..0e9cef728 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktLayout.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktLayout.kt @@ -7,7 +7,19 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.rokt.roktsdk.PlacementOptions import com.rokt.roktsdk.RoktConfig +import com.rokt.roktsdk.RoktEvent +/** + * Rokt Jetpack Compose placement wrapper with mParticle attribute enrichment. + * + * @param sdkTriggered Whether the Rokt SDK should trigger placement selection. + * @param identifier The placement identifier. + * @param attributes Attributes to enrich through mParticle before rendering. + * @param location The Rokt placement location. + * @param modifier Optional Compose modifier for the placement. + * @param config Optional Rokt SDK configuration. + * @param onEvent Callback for native Rokt SDK placement events. + */ @Composable @Suppress("FunctionName") fun RoktLayout( @@ -17,6 +29,7 @@ fun RoktLayout( location: String, modifier: Modifier = Modifier, config: RoktConfig? = null, + onEvent: (RoktEvent) -> Unit = {}, ) { var placementOptions: PlacementOptions? = null val instance = RoktKit.instance @@ -43,6 +56,7 @@ fun RoktLayout( location = location, config = config, placementOptions = placementOptions, + onEvent = onEvent, ) } } diff --git a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt index 07701ea7a..0433b8a57 100644 --- a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt +++ b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt @@ -54,6 +54,11 @@ class RoktTest { private lateinit var configManager: FakeConfigManager private lateinit var rokt: Rokt + private data class RoktFacadeFixture( + val mParticle: MParticle, + val roktListener: RoktKitBridge, + ) + class FakeConfigManager(var enabled: Boolean = true) { fun isEnabled(): Boolean = enabled } @@ -279,4 +284,60 @@ class RoktTest { ) assertTrue(optionsCaptor.value.jointSdkSelectPlacements >= currentTimeMillis) } + + @Test + fun testMParticleRoktExtensionEvents_delegatesToCurrentInstance() { + val fixture = createMParticleWithRoktKit() + MParticle.setInstance(fixture.mParticle) + val expectedFlow: Flow = flowOf() + `when`(fixture.roktListener.events("identifier")).thenReturn(expectedFlow) + + val result = MParticle.getInstance()!!.rokt.events("identifier") + + verify(fixture.roktListener).events("identifier") + assertEquals(expectedFlow, result) + } + + @Test + fun testMParticleRoktExtensionUsesNewFacadeAfterInstanceChanges() { + val firstFixture = createMParticleWithRoktKit() + val secondFixture = createMParticleWithRoktKit() + val firstFlow: Flow = flowOf() + val secondFlow: Flow = flowOf() + `when`(firstFixture.roktListener.events("first")).thenReturn(firstFlow) + `when`(secondFixture.roktListener.events("second")).thenReturn(secondFlow) + + MParticle.setInstance(firstFixture.mParticle) + val firstResult = MParticle.getInstance()!!.rokt.events("first") + + MParticle.setInstance(secondFixture.mParticle) + val secondResult = MParticle.getInstance()!!.rokt.events("second") + + assertEquals(firstFlow, firstResult) + assertEquals(secondFlow, secondResult) + verify(firstFixture.roktListener).events("first") + verify(secondFixture.roktListener).events("second") + verify(firstFixture.roktListener, never()).events("second") + } + + private fun createMParticleWithRoktKit(): RoktFacadeFixture { + val mParticle = org.mockito.Mockito.mock(MParticle::class.java) + val internal = org.mockito.Mockito.mock(MParticle.Internal::class.java) + val kitManager = + org.mockito.Mockito.mock(MParticle.Internal::class.java.getMethod("getKitManager").returnType) as KitManager + val roktKit = + org.mockito.Mockito.mock( + KitIntegration::class.java, + withSettings().extraInterfaces(RoktKitBridge::class.java), + ) + val roktListener = roktKit as RoktKitBridge + + `when`(mParticle.Internal()).thenReturn(internal) + org.mockito.Mockito.doReturn(kitManager).`when`(internal).kitManager + `when`(kitManager.isEnabled).thenReturn(true) + `when`(kitManager.isKitActive(MParticle.ServiceProviders.ROKT)).thenReturn(true) + `when`(kitManager.getKitInstance(MParticle.ServiceProviders.ROKT)).thenReturn(roktKit) + + return RoktFacadeFixture(mParticle, roktListener) + } }