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) + } }