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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<!-- markdownlint-disable MD024 MD041 -->

## [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

Expand Down
36 changes: 35 additions & 1 deletion kits/rokt/rokt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,42 @@ object MParticleRokt {
@Volatile
private var rokt: Rokt? = null

@Volatile
private var roktInstance: MParticle? = null

@Suppress("FunctionName")
@JvmStatic
fun Rokt(): Rokt {
val mParticle = requireNotNull(MParticle.getInstance()) {
"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)
Expand Down
14 changes: 14 additions & 0 deletions kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -43,6 +56,7 @@ fun RoktLayout(
location = location,
config = config,
placementOptions = placementOptions,
onEvent = onEvent,
)
}
}
61 changes: 61 additions & 0 deletions kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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<RoktEvent> = 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<RoktEvent> = flowOf()
val secondFlow: Flow<RoktEvent> = 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)
}
}
Loading